Использование ScrollableResults Hibernate для медленного чтения 90 миллионов записей

Мне просто нужно прочитать каждую строку в таблице в моей базе данных 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 ответов

Решение

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

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

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

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

РЕДАКТИРОВАТЬ:

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

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

select * 
from person 
where id > <max_id_of_last_batch> and <other_conditions> 
order by id asc  
limit <batch_size>

Вы должны быть в состоянии использовать 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);

Это можно сделать с помощью интерфейса Query (это должно работать и для Criteria) в версии 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, хотя вы должны прочитать его документацию заранее."

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

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

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
        .setReadOnly(true)
        .setFetchSize( 1000 ) // <<--- !!!!
        .setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)

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

Это должно быть буквально взято из официальной ссылки: https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

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

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

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

С 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.

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

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

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

До этого я успешно использовал функцию прокрутки Hibernate, не читая весь набор результатов. Кто-то сказал, что MySQL не выполняет настоящие курсоры прокрутки, но утверждает, что основывается на JDBC dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE) и обыскивает его. кажется, что другие люди использовали это. Убедитесь, что он не кэширует объекты Person в сеансе - я использовал его в SQL-запросах, где не было сущности для кэширования. Вы можете вызвать evict в конце цикла или проверить его с помощью SQL-запроса. Также поиграйте с setFetchSize, чтобы оптимизировать количество поездок на сервер.

Недавно я работал над такой проблемой, и я написал блог о том, как решить эту проблему. очень похоже, я надеюсь быть полезным для любого. я использую ленивый подход списка с частичным adquisition. я заменил предел и смещение или нумерацию запроса на нумерацию страниц вручную. В моем примере select возвращает 10 миллионов записей, я получаю их и вставляю во "временную таблицу":

create or replace function load_records ()
returns VOID as $$
BEGIN
drop sequence if exists temp_seq;
create temp sequence temp_seq;
insert into tmp_table
SELECT linea.*
FROM
(
select nextval('temp_seq') as ROWNUM,* from table1 t1
 join table2 t2 on (t2.fieldpk = t1.fieldpk)
 join table3 t3 on (t3.fieldpk = t2.fieldpk)
) linea;
END;
$$ language plpgsql;

после этого я могу разбивать на страницы без подсчета каждой строки, но используя назначенную последовательность:

select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000

С точки зрения Java, я реализовал эту нумерацию страниц посредством частичного размещения с ленивым списком. это список, который выходит из списка Abstract и реализует метод get(). Метод get может использовать интерфейс доступа к данным для продолжения получения следующего набора данных и освобождения кучи памяти:

@Override
public E get(int index) {
  if (bufferParcial.size() <= (index - lastIndexRoulette))
  {
    lastIndexRoulette = index;
    bufferParcial.removeAll(bufferParcial);
    bufferParcial = new ArrayList<E>();
        bufferParcial.addAll(daoInterface.getBufferParcial());
    if (bufferParcial.isEmpty())
    {
        return null;
    }

  }
  return bufferParcial.get(index - lastIndexRoulette);<br>
}

с другой стороны, интерфейс доступа к данным использует запрос для разбивки на страницы и реализует один метод для последовательной итерации, каждые 25000 записей для завершения всего этого.

результаты для этого подхода можно увидеть здесь http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

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

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

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

Другие вопросы по тегам