В новом переводе от команды Spring АйО Гевин Кинг, создатель Hibernate, объясняет, почему значение fetch size по умолчанию в драйвере Oracle может замедлять запросы — и как его изменение помогает повысить масштабируемость системы.
На прошлой неделе Йерон Боргерс попросил в Твиттере, чтобы мы создали стандартный способ задать JDBC fetch size в JPA, то есть, добавить функцию Hibernate Query.setFetchSize()
к стандартным API. Это меня несколько удивило, потому что никто не просил об этом раньше, но я попросил его открыть тикет. После некоторых дискуссий я убедился в том, что его реальные потребности можно удовлетворить другим способом, но дискуссия привлекла мое внимание к важному факту: по умолчанию JDBC fetch size для драйвера Oracle равен 10.
Я никогда не претендовал на статус эксперта в настройке производительности Oracle, и я не использую Oracle каждый день. Тем не менее, мне казалось, что я должен был знать об этом факте после стольких лет работы с JDBC.
Из любопытства я провел голосование в Твиттере, которое помимо прочих перепостил Франк Пашот (прим. ред. Франк Пашот — эксперт по базам данных, работающий с PostgreSQL, Oracle, MongoDB и AWS, а также продвигающий YugabyteDB.):
Перевод твита:
Вопрос для всех, кто использует базы данных Oracle с Java. Пожалуйста, скажите нам:
Знаете ли вы JDBC fetch size по умолчанию для Oracle?
Переопределяете ли вы это значение по умолчанию или оставляете его равным значению по умолчанию?
Знаю + оставляю 9,9%
Знаю + переопределяю 20,7%
Не знаю 43,8%
Что такое JDBC fetch size? 25,6%
Теперь посмотрите, всего проголосовал 121 человек — это нормальный размер выборки, но, конечно, эта выборка не была репрезентативной. Какого искажения результатов мы могли ожидать в сравнении с типичной случайной выборкой из числа разработчиков? Ну, мне хотелось думать, что мои подписчики знают намного больше о базах данных, чем большинство людей, и я даже с еще большей уверенностью сказал бы это о подписчиках Франка.
Я действительно хотел узнать мнение пользователей Oracle, но столкнулся с проблемой: среди тех, кто выбрал «Что такое JDBC fetch size?», могли быть те, кто просто хотел посмотреть результаты. Так как в моем опросе не было отдельной опции «Посмотреть результаты», я исключил из анализа всех, кто признался, что не знает, что это такое, и сосредоточился на ответах остальных респондентов, чтобы минимизировать искажения.
Более 70% респондентов заявляют, что используют Oracle и либо не знают, чему равна fetch size, либо знают, но не меняют это значение.
Я связался с Лоиком Лефевром из Oracle, чтобы убедиться, что я хорошо понимаю ситуацию. Он и его коллега Коннор Макдональд указали мне на то, что на самом деле JDBC драйвер для Oracle имеет адаптивный fetch size в версии 23ai, и что при оптимальном сценарии драйвер в действительности увеличит fetch size до 250 на четвертой загрузке данных. Более того, это поведение зависит от размера каждой записи в результирующем наборе данных. Я был рад узнать об этом.
Теперь выдвину несколько утверждений:
Большая часть кода для доступа данных в Java занимается OLTP, а не обработкой бачей
Для таких программ большинство запросов возвращают от 100 до 102 записей, при этом результат в 103 записей возможен, но случается весьма редко. В отличии от этих значений, величины в 104 записей и выше характеризуют оффлайн обработку бачей.
Размер каждой записи в результате такого запроса как правило не очень велик.
Общепринятая практика — особенно для программ, использующих Hibernate или JPA — ограничивать количество извлекаемых данных с применением
LIMIT
, считывать весь результирующий набор данных JDBC одновременно, помещая результаты вList
или что-то похожее и затем продолжать работать с этим списком.Для клиента, написанного на Java, абсолютно нормально находиться на другой физической машине по отношению к серверу базы данных.
Для клиента, написанного на Java, абсолютно нормально иметь доступ к большому количеству памяти.
Сервер базы данных, как правило, является наименее масштабируемым элементом системы.
При обработке онлайн-транзакций нас очень сильно волнует вопрос о latency.
Конечно, можно легко представить себе сценарии, для которых одно или несколько из этих предположений будут нарушены. Да, да, да, я прекрасно знаю, что некоторые люди занимаются обработкой бачей на Java. Коментарии, которые я приведу далее, не относятся к обработке бачей. Но я настаиваю на том, что то, что я описал выше, является достаточно точным описанием самого распространенного случая.
Теперь давайте посмотрим на то, что случается с запросом, возвращающим 12 записей:
При первом визите на сервер базы данных сервер выполняет запрос, формирует набор результатов в памяти и затем возвращает клиенту 10 записей.
Java клиент проходится по этим записям, создавая граф Java объектов и помещая их в список, затем блокируется в ожидании следующих 10 записей.
JDBC драйвер запрашивает оставшиеся 2 записи с сервера, заставляя систему ждать.
Теперь Java клиент может обработать оставшиеся две записи и наконец-то продолжить заниматься тем, что он делал до этого.
Это плохо.
Мы не только два раза сходили на сервер вместо одного, мы также заставили сервер поддерживать ассоциированное с клиентом состояние во время всех этих взаимодействий. Я повторю: сервер базы данных обычно является наименее масштабируемым слоем. И у нас почти никогда не возникает необходимости в том, чтобы сервер базы данных оставался в некоем постоянном состоянии, дожидаясь, пока клиент что-то сделает.
Для запроса, который возвращает 50 записей, история еще хуже. Даже в лучшем случае поведение драйвера по умолчанию требует четыре похода на сервер базы данных, чтобы получить эти 50 записей. Поверьте мне, нормальная JVM не бабахнет с ошибкой Out of Memory только потому, что вы отправили сразу 50 записей!
Поэтому мои рекомендации следующие:
JDBC fetch size по умолчанию должен устанавливаться в большое число, где-то между 103 и 231-1. Этим значением можно управлять через
hibernate.jdbc.fetch_size
, или, что еще лучше, на Oracle, через свойствоdefaultRowPrefetch
у JDBC подключения. Заметьте, что большинство JDBC драйверов по умолчанию имеют неограниченный fetch size, и я считаю, что это самое лучшее значение по умолчанию.Используйте пагинацию через SQL параметр
LIMIT
, то есть, стандартный JPAsetMaxResults()
API, чтобы контролировать размер результирующего набора, если это необходимо. Помните: если вы вызываете функциюgetResultList()
от JPA, установка меньшего fetch size абсолютно не поможет управлять количеством получаемых данных, поскольку JPA провайдер все равно прочтет их в режимеeager
и положит в список!Для особых случаев вроде обработки бачей для огромных массивов данных используйте StatelessSession или Session.clear(), чтобы управлять использованием памяти со стороны Java, и ScrollableResults вместе с setFetchSize() для управления загрузкой. Или, что еще лучше, сделайте свою жизнь проще и напишите сохраненную процедуру.
Так что, если вы принадлежите к этим 70% пользователей Oracle, у вас должна появиться возможность сделать вашу программу более быстрой в части отправки ответов и более масштабируемой, почти не сделав при этом никакой работы, используя только этот Один Простой Трюк.
ПОСТСКРИПТУМ: Заметьте, что если вы попытаетесь искать "JDBC fetch size" в Гугле, вы найдете кучу неправильной информации. Единственный источник, где все написано правильно, принадлежит (совпадение?) Франку Пашоту.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.