Подтвердить что ты не робот

Использование 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

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

4b9b3361

Ответ 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)

Ответ 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