Использование Hibernate ScrollableResults для медленного чтения 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 heap space :(.

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

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

UPDATE: setFirstResult / setMaxResults не работает, оказывается, занимает непригодно много времени, чтобы добраться до смещений, как я боялся. Здесь должно быть решение! Разве это не стандартная процедура?? Я готов отказаться от спящего режима и использовать JDBC или что угодно.

UPDATE 2: решение, которое я придумал, которое работает нормально, не отлично, в основном имеет форму:

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

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

12 ответов


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

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

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

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

изменить:

ваше обновление 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>

вы должны иметь возможность использовать 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 + спящего режима 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 lazy-loading, потому что он выдаст исключение для любых запросов, выполняемых до завершения прокрутки.


С 90 миллионами записей, это звучит так, как вы должны дозировать ваши выбирает. Я закончил с 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 не делает истинных курсоров прокрутки, но он утверждает, что основан на dmd JDBC.supportsResultSetType (ResultSet.TYPE_SCROLL_INSENSITIVE) и поиск вокруг кажется, что другие люди использовали его. Убедитесь, что он не кэширует объекты Person в сеансе - я использовал его в SQL-запросах, где не было сущности для кэширования. Вы можете вызвать выселение в конце цикл, чтобы убедиться или проверить с помощью 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, я реализовал эту разбиение на страницы через частичное adquisition с ленивым списком. это список, который простирается от абстрактного списка и реализует метод 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


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


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

с JDBC:jtds:SQLServer, в://имя localhost:1433/САУ;ТДС=8.0;useCursors=истина