Использование 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: (.
Итак, я думаю, ScrollableResults не то, что я искал? Каков правильный способ справиться с этим? Я не возражаю, если это время цикла занимает несколько дней (ну, я бы хотел, чтобы это не было).
Я думаю, что единственный способ справиться с этим - использовать setFirstResult и setMaxResults для повторения результатов и просто использовать регулярные результаты Hibernate вместо ScrollableResults. Похоже, что это будет неэффективно, хотя и начнет смеяться долгое время, когда я назову setFirstResult на 89-миллионной строке...
UPDATE: setFirstResult/setMaxResults не работает, получается, что требуется слишком много времени, чтобы добраться до смещений, как я боялся. Здесь должно быть решение! Разве это не стандартная процедура? Я готов отказаться от Hibernate и использовать JDBC или что-то еще, что нужно.
ОБНОВЛЕНИЕ 2: решение, которое я придумал, работает нормально, а не отлично, в основном имеет форму:
select * from person where id > <offset> and <other_conditions> limit 1
Поскольку у меня есть другие условия, даже все в индексе, это все еще не так быстро, как хотелось бы... так что все еще открыта для других предложений.
Ответы
Ответ 1
Использование setFirstResult и setMaxResults - это ваш единственный вариант, о котором я знаю.
Традиционно прокручиваемый набор результатов будет передавать строки только клиенту по мере необходимости. К сожалению, MySQL Connector/J на самом деле подделывает его, он выполняет весь запрос и передает его клиенту, поэтому у драйвера на самом деле есть весь набор результатов, загруженный в ОЗУ, и он будет капать его вам (о чем свидетельствуют проблемы с вашей памятью), У вас была правильная идея, это просто недостатки в Java java-драйвере.
Я не нашел возможности обойти это, поэтому пошел с загрузкой больших кусков, используя обычные методы setFirst/max. Извините за то, что вы плохо знаете.
Просто убедитесь, что используете сеанс без состояния, чтобы не было кеша уровня сеанса или грязного отслеживания и т.д.
EDIT:
Ваш UPDATE 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>
Ответ 2
Вы должны иметь возможность использовать 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 (это также должно работать и для критериев) в версии 3.2+ API-интерфейса Hibernate:
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
, хотя вы должны прочитать его документацию заранее.
Ответ 3
Задайте размер выборки в запросе на оптимальное значение, как показано ниже.
Кроме того, когда кеширование не требуется, может быть лучше использовать StatelessSession.
ScrollableResults results = session.createQuery( "SELECT person FROM Person person" ) .setReadOnly(true). setFetchSize (1000).setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)
Ответ 4
FetchSize должен быть Integer.MIN_VALUE
, иначе он не будет работать.
Он должен быть буквально взят из официальной ссылки: http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html
Ответ 5
На самом деле вы могли бы получить то, что хотели - прокручиваемые с низкой памятью результаты с MySQL - если бы вы использовали упомянутый здесь ответ:
Потоковая передача больших наборов результатов с MySQL
Обратите внимание, что у вас будут проблемы с Hibernate lazy-load, потому что он будет генерировать исключение для любых запросов, выполняемых до завершения прокрутки.
Ответ 6
С 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
.
Другим преимуществом этого будет parallelism - вы можете выполнять несколько потоков параллельно для этого для более быстрой обработки.
Обработка 90 миллионов записей также не похожа на сладкое место для использования Hibernate.
Ответ 7
Проблема может заключаться в том, что Hibernate сохраняет ссылки на все объекты в сеансе до закрытия сеанса. Это не имеет никакого отношения к кешированию запросов. Возможно, это поможет вывести() объекты из сеанса после того, как вы закончите запись объекта в файл. Если они больше не ссылаются на сеанс, сборщик мусора может освободить память, и вы больше не будете исчерпать память.
Ответ 8
Я предлагаю более чем пример кода, но шаблон запроса на основе Hibernate
для этого обходного пути для вас (pagination
, scrolling
и clearing
сеанс Hibernate).
Он также может быть легко адаптирован для использования EntityManager
.
Ответ 9
Я успешно использовал функциональные возможности прокрутки Hibernate до того, как не прочитал весь набор результатов. Кто-то сказал, что MySQL не выполняет настоящие курсоры прокрутки, но утверждает, что он основан на JDBC dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE) и поиск вокруг кажется, что другие люди его использовали. Убедитесь, что он не кэширует объекты Person в сеансе - я использовал его в SQL-запросах, где не было сущности для кэширования. Вы можете вызвать evict в конце цикла, чтобы убедиться или проверить с помощью SQL-запроса. Также поиграйте с setFetchSize, чтобы оптимизировать количество поездок на сервер.
Ответ 10
В последнее время я работал над проблемой, подобной этой, и я написал блог о том, как сталкивается с этой проблемой. очень нравится, я надеюсь быть полезным для любого.
Я использую метод ленивого списка с частичным приложением. я Заменен лимит и смещение или разбиение на страницы запроса на ручную разбивку на страницы.
В моем примере выбор возвращает 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, я реализовал эту разбивку по страницам через частичное объявление с ленивым списком. это список, который простирается от абстрактного списка и реализует метод 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
Ответ 11
Другой вариант, если вы "исчерпали ОЗУ", - это просто запросить сказать, один столбец вместо всего объекта Как использовать критерии спящего режима, чтобы вернуть только один элемент объект вместо всего объекта? (экономит много процессорного времени для загрузки).