Автор оригинала: Vlad Mihalcea.
Вступление
Я прочитал очень интересную статью Кресимира Косича о потоковой передаче набора результатов MySQL, когда речь заходит о сокращении использования памяти.
Марк Палуч из Spring Data спросил , можем ли мы включить потоковую передачу результирующего набора MySQL по умолчанию всякий раз, когда мы используем Запрос#поток
или Запрос#прокрутка
.
Тем не менее, проблема HHH-11260 была создана, и я начал работать над ней. Во время экспертной оценки Стив Эберсол (руководитель группы Hibernate ORM) и Санне Гриноверо (Руководитель группы поиска Hibernate) выразили озабоченность по поводу внесения таких изменений.
Прежде всего, Потоковая передача результирующего набора MySQL содержит следующие предостережения:
-
результирующий набор
должен быть пройден полностью перед выполнением любой другой инструкции SQL - оператор не закрывается, если в связанном наборе результатов
все еще есть записи, которые нужно прочитать
- блокировки, связанные с базовой инструкцией SQL, которая передается в потоковом режиме, снимаются после завершения транзакции (либо фиксации, либо отката).
Как работает потоковая передача набора результатов MySQL по сравнению с одновременной выборкой всего набора результатов JDBC @vlad_mihalcea https://t.co/GhQ0ucJSjx pic.twitter.com/5ptqdyuPmG
В подавляющем большинстве ситуаций потоковая передача набора результатов не требуется по следующим причинам:
- если вам нужно обработать большой объем данных, гораздо эффективнее обрабатывать их в базе данных с помощью хранимой процедуры. Это особенно верно для Oracle и SQL Server, которые предлагают очень надежный процедурный язык.
- если вам нужно обработать данные в приложении, то пакетная обработка – это правильный путь. При этом вам нужно только выбрать и обработать небольшие объемы данных за один раз. Это позволяет предотвратить длительные транзакции, которые нежелательны как для 2PL , так и для |/MVCC |/транзакций базы данных . Разделив набор данных на несколько пакетов, вы сможете лучше распараллелить задачу обработки данных.
При этом единственная причина, по которой вы должны использовать потоковую передачу, заключается в ограничении выделения памяти на стороне клиента, избегая при этом выполнения инструкции SQL для каждого пакетного выполнения.
Однако выдача новой инструкции, которая извлекает текущие пакетные данные, может быть реальным преимуществом, поскольку запрос может быть разбит на страницы. Если отфильтрованный набор данных довольно большой, то вам следует использовать Разбиение на страницы набора ключей , как объясняет Маркус Винанд в своей книге Производительность SQL объяснена . Если результирующий набор не слишком велик, то решением также может быть смещенная разбивка на страницы.
Еще одним большим преимуществом небольших постраничных запросов является избирательность индекса. Если отфильтрованный набор данных довольно велик, возможно, вы не сможете извлечь выгоду из индексации, поскольку план выполнения решил вместо этого подать в суд на последовательное сканирование. Поэтому потоковый запрос может быть медленным.
Разбитый на страницы запрос, который должен сканировать небольшой набор данных, может лучше использовать индекс базы данных, поскольку стоимость произвольного доступа может быть ниже, чем стоимость, связанная с последовательным сканированием.
Как работает потоковая передача MySQL?
Если вы путаете весь поток, как это делает Кресимир Несик в своей статье, то, возможно, вам лучше использовать пакетную обработку.
Давайте посмотрим, что быстрее, когда дело доходит до использования всего набора результатов
выборки по умолчанию или альтернативы потоковой передачи.
Выборка по умолчанию-все делается следующим образом:
private void stream(EntityManager entityManager) { final AtomicLong sum = new AtomicLong(); try(StreampostStream = entityManager .createQuery("select p from Post p", Post.class) .setMaxResults(resultSetSize) .unwrap(Query.class) .stream()) { postStream.forEach(post -> sum.incrementAndGet()); } assertEquals(resultSetSize, sum.get()); }
в то время как потоковая передача драйверов JDBC выполняется с помощью org.hibernate.Размер выборки
Спящий режим Запрос
подсказка:
private void stream(EntityManager entityManager) { final AtomicLong sum = new AtomicLong(); try(StreampostStream = entityManager .createQuery("select p from Post p", Post.class) .setMaxResults(resultSetSize) .setHint(QueryHints.HINT_FETCH_SIZE, Integer.MIN_VALUE) .unwrap(Query.class) .stream()) { postStream.forEach(post -> sum.incrementAndGet()); } assertEquals(resultSetSize, sum.get()); }
Чтобы включить потоковую передачу при использовании MySQL, вам либо нужно установить размер выборки JDBC в Целое число.MIN_VALUE
или используйте положительное целое значение, если вы также установили для свойства useCursorFetch
connection значение true
. Для нашего тестового случая любой из вариантов дал аналогичные результаты.
Тест выполняет прогрев 25 000 вызовов методов, а затем выполняет метод stream
10 000 раз, измеряя время выборки с помощью метрик Dropwizard .
На оси y на диаграмме показан 98-й процентиль, который был записан Dropwizard Таймером
при использовании всего набора результатов
. На оси x Размер набора результатов
варьируется от 1, 2, 5 до более высоких значений (например, 5000).
Время отклика увеличивается с увеличением размера результирующего набора. Поэтому в приложениях OLTP вы всегда должны стремиться к тому, чтобы JDBC Набор результатов
был как можно меньше. Вот почему запросы пакетной обработки и разбиения на страницы обычно являются лучшей альтернативой потоковой передаче большого набора результатов.
Код доступен на GitHub .
Вывод
Предположения Стива и Энн оказались верными. Потоковая передача работает хуже, чем просто получение всего набора результатов
сразу, что является стратегией по умолчанию как для драйверов JDBC MySQL, так и для PostgreSQL.
Поэтому не рекомендуется вносить изменения, предложенные в выпуске HHH-11260 Jira. Тем не менее, вам решать, имеет ли смысл потоковая передача для вашего варианта использования или вам следует использовать пакетную обработку с разбиением на страницы запросов.