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