Использование ScrollableResults Hibernate для медленного чтения 90 миллионов записей - PullRequest
50 голосов
/ 13 мая 2010

Мне просто нужно прочитать каждую строку в таблице в моей базе данных MySQL, используя Hibernate, и записать файл на ее основе. Но есть 90 миллионов строк, и они довольно большие. Таким образом, казалось, что было бы целесообразно следующее:

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
            .setReadOnly(true).setCacheable(false).scroll(ScrollMode.FORWARD_ONLY);
while (results.next())
    storeInFile(results.get()[0]);

Проблема в том, что описанное выше попытается загрузить все 90 миллионов строк в ОЗУ, прежде чем перейти к циклу while ... и это уничтожит мою память с помощью OutOfMemoryError: исключения пространства кучи Java: (..

Итак, я думаю, ScrollableResults - это не то, что я искал? Как правильно справиться с этим? Я не против, если этот цикл занимает дни (ну, я бы с удовольствием это сделал).

Полагаю, единственный другой способ справиться с этим - использовать setFirstResult и setMaxResults для перебора результатов и просто использовать обычные результаты Hibernate вместо ScrollableResults. Такое ощущение, что это будет неэффективно, и начнёт занимать смехотворно много времени, когда я вызову setFirstResult для 89-миллионной строки ...

ОБНОВЛЕНИЕ: setFirstResult / setMaxResults не работает, оказывается, что до смещения, как я боялся, уходит необычайно много времени. Здесь должно быть решение! Разве это не довольно стандартная процедура? Я готов отказаться от Hibernate и использовать JDBC или что-то еще.

ОБНОВЛЕНИЕ 2: предложенное мной решение, которое работает нормально, не очень, в основном имеет вид:

select * from person where id > <offset> and <other_conditions> limit 1

Поскольку у меня есть другие условия, даже все в индексе, это все еще не так быстро, как хотелось бы ... так что все еще открыты для других предложений ..

Ответы [ 12 ]

29 голосов
/ 13 мая 2010

Использование setFirstResult и setMaxResults - ваша единственная опция, о которой я знаю.

Традиционно прокручиваемый набор результатов будет передавать строки клиенту только по мере необходимости. К сожалению, MySQL Connector / J фактически подделывает его, он выполняет весь запрос и передает его клиенту, поэтому драйвер фактически загружает весь набор результатов в ОЗУ и передает его вам по капле (о чем свидетельствуют проблемы с нехваткой памяти) , Вы правильно поняли, это просто недостатки в Java-драйвере MySQL.

Я не нашел способа обойти это, поэтому пошел с загрузкой больших кусков, используя обычные методы setFirst / max. Извините, что принес плохие новости.

Просто убедитесь, что вы используете сеанс без сохранения состояния, чтобы не было кэша на уровне сеанса или грязного отслеживания и т. Д.

EDIT:

Ваше ОБНОВЛЕНИЕ 2 - лучшее, что вы получите, если не выйдете из MySQL J / Connector. Хотя нет причин, по которым вы не можете увеличить лимит запроса. Если у вас достаточно оперативной памяти для хранения индекса, это будет довольно дешевой операцией. Я бы немного изменил его, и взял бы пакет за раз, и использовал бы самый высокий идентификатор этого пакета, чтобы получить следующий пакет.

Примечание: это будет работать только в том случае, если other_conditions использует равенство (не допускаются условия диапазона) и имеет последний столбец индекса как id .

select * 
from person 
where id > <max_id_of_last_batch> and <other_conditions> 
order by id asc  
limit <batch_size>
19 голосов
/ 31 июля 2012

Вы должны иметь возможность использовать ScrollableResults, хотя для работы с MySQL требуется несколько магических заклинаний. Я написал свои выводы в блоге (http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/), но я подведу итоги здесь:

"Документация [JDBC] гласит:

To enable this functionality, create a Statement instance in the following manner:
stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
                java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

Это можно сделать с помощью интерфейса запросов (это также должно работать для критериев) в версии 3.2+ Hibernate API:

Query query = session.createQuery(query);
query.setReadOnly(true);
// MIN_VALUE gives hint to JDBC driver to stream results
query.setFetchSize(Integer.MIN_VALUE);
ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
// iterate over results
while (results.next()) {
    Object row = results.get();
    // process row then release reference
    // you may need to evict() as well
}
results.close();

Это позволяет вам выполнять потоковую передачу по результирующему набору, однако Hibernate все равно будет кэшировать результаты в Session, поэтому вам придется вызывать session.evict() или session.clear() каждый раз. Если вы только читаете данные, вы можете рассмотреть возможность использования StatelessSession, хотя вам следует заранее прочитать его документацию. "

17 голосов
/ 25 января 2011

Установите размер выборки в запросе на оптимальное значение, как указано ниже.

Кроме того, когда кэширование не требуется, может быть лучше использовать StatelessSession.

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
        .setReadOnly(true)
        .setFetchSize( 1000 ) // <<--- !!!!
        .setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)
7 голосов
/ 18 января 2012

FetchSize должен быть Integer.MIN_VALUE, иначе он не будет работать.

Буквально взято из официального справочника: https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

3 голосов
/ 10 декабря 2010

На самом деле вы могли бы получить то, что хотели - результаты прокручивания при малой памяти с MySQL - если бы использовали ответ, упомянутый здесь:

Потоковые большие наборы результатов с MySQL

Обратите внимание, что у вас будут проблемы с отложенной загрузкой Hibernate, потому что это вызовет исключение для любых запросов, выполненных до завершения прокрутки.

1 голос
/ 07 апреля 2013

Я предлагаю более пример кода , но шаблон запроса на основе Hibernate, чтобы сделать этот обходной путь для вас (pagination, scrolling и clearing Hibernate сеанс).

Он также может быть легко адаптирован для использования EntityManager.

1 голос
/ 15 июля 2010

Проблема может заключаться в том, что Hibernate хранит ссылки на все объекты в сеансе, пока вы не закроете сеанс. Это не имеет ничего общего с кэшированием запросов. Может быть, это поможет удалить объекты из сеанса после того, как вы закончите запись объекта в файл. Если они больше не являются ссылками в сеансе, сборщик мусора может освободить память, и вам больше не хватит памяти.

1 голос
/ 13 мая 2010

С 90 миллионами записей кажется, что вы должны пакетировать свои SELECT.Я сделал с Oracle, когда делал начальную загрузку в распределенный кеш.Глядя на документацию MySQL, кажется, что эквивалент использует предложение LIMIT: http://dev.mysql.com/doc/refman/5.0/en/select.html

Вот пример:

SELECT * from Person
LIMIT 200, 100

Это вернуло бы строки с 201 по 300 из Personтаблица.

Сначала вам нужно будет получить количество записей из таблицы, а затем разделить его на размер пакета и обработать ваши циклы и параметры LIMIT.

ДругойПреимуществом этого будет параллелизм - вы можете выполнять несколько потоков параллельно для ускорения обработки.

Обработка 90 миллионов записей также не является подходящим местом для использования Hibernate.

0 голосов
/ 27 апреля 2018

Для меня это работало правильно при установке useCursors = true, в противном случае Scrollable Resultset игнорирует все реализации размера выборки, в моем случае это было 5000, но Scrollable Resultset извлек миллионы записей одновременно, вызывая чрезмерное использование памяти. лежащая в основе БД - MSSQLServer.

JDBC: jtds: SQLServer: // локальный: 1433 / ACS; TDS = 8,0; useCursors = истина

0 голосов
/ 17 апреля 2015

Другой вариант, если вам «не хватает оперативной памяти» - просто запросить, скажем, один столбец вместо всего объекта. Как использовать критерии гибернации, чтобы вернуть только один элемент объекта вместо всего объекта? (экономит много времени процессора при загрузке).

...