В новом переводе от команды 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 на четвертой загрузке данных. Более того, это поведение зависит от размера каждой записи в результирующем наборе данных. Я был рад узнать об этом.

Теперь выдвину несколько утверждений:

  1. Большая часть кода для доступа данных в Java занимается OLTP, а не обработкой бачей

  2. Для таких программ большинство запросов возвращают от 100 до 102 записей, при этом результат в 103 записей возможен, но случается весьма редко. В отличии от этих значений, величины в 104 записей и выше характеризуют оффлайн обработку бачей.

  3. Размер каждой записи в результате такого запроса как правило не очень велик.

  4. Общепринятая практика —​ особенно для программ, использующих Hibernate или  JPA —​ ограничивать количество извлекаемых данных с применением LIMIT, считывать весь результирующий набор данных JDBC одновременно, помещая результаты в List или что-то похожее и затем продолжать работать с этим списком.

  5. Для клиента, написанного на Java, абсолютно нормально находиться на другой физической машине по отношению к серверу базы данных.

  6. Для клиента, написанного на Java, абсолютно нормально иметь доступ к большому количеству памяти.

  7. Сервер базы данных, как правило, является наименее масштабируемым элементом системы.

  8. При обработке онлайн-транзакций нас очень сильно волнует вопрос о latency.

Конечно, можно легко представить себе сценарии, для которых одно или несколько из этих предположений будут нарушены. Да, да, да, я прекрасно знаю, что некоторые люди занимаются обработкой бачей на Java. Коментарии, которые я приведу далее, не относятся к обработке бачей. Но я настаиваю на том, что то, что я описал выше, является достаточно точным описанием самого распространенного случая.

Теперь давайте посмотрим на то, что случается с запросом, возвращающим 12 записей:

  1. При первом визите на сервер базы данных сервер выполняет запрос, формирует набор результатов в памяти и затем возвращает клиенту 10 записей.

  2. Java клиент проходится по этим записям, создавая граф Java объектов и помещая их в список, затем блокируется в ожидании следующих 10 записей.

  3. JDBC драйвер запрашивает оставшиеся 2 записи с сервера, заставляя систему ждать.

  4. Теперь Java клиент может обработать оставшиеся две записи и наконец-то продолжить заниматься тем, что он делал до этого.

Это плохо.

Мы не только два раза сходили на сервер вместо одного, мы также заставили сервер поддерживать ассоциированное с клиентом состояние во время всех этих взаимодействий. Я повторю: сервер базы данных обычно является наименее масштабируемым слоем. И у нас почти никогда не возникает необходимости в том, чтобы сервер базы данных оставался в некоем постоянном состоянии, дожидаясь, пока клиент что-то сделает.

Для запроса, который возвращает 50 записей, история еще хуже. Даже в лучшем случае поведение драйвера по умолчанию требует четыре похода на сервер базы данных, чтобы получить эти 50 записей. Поверьте мне, нормальная JVM не бабахнет с ошибкой Out of Memory только потому, что вы отправили сразу 50 записей!

Поэтому мои рекомендации следующие:

  1. JDBC fetch size по умолчанию должен устанавливаться в большое число, где-то между 103 и 231-1. Этим значением можно управлять через hibernate.jdbc.fetch_size, или, что еще лучше, на Oracle, через свойство defaultRowPrefetch у JDBC подключения. Заметьте, что большинство JDBC драйверов по умолчанию имеют неограниченный fetch size, и я считаю, что это самое лучшее значение по умолчанию.

  2. Используйте пагинацию через SQL параметр LIMIT, то есть, стандартный JPA setMaxResults() API, чтобы контролировать размер результирующего набора, если это необходимо. Помните: если вы вызываете функцию getResultList() от JPA, установка меньшего fetch size абсолютно не поможет управлять количеством получаемых данных, поскольку JPA провайдер все равно прочтет их в режиме eager и положит в список!

  3. Для особых случаев вроде обработки бачей для огромных массивов данных используйте StatelessSession или Session.clear(), чтобы управлять использованием памяти со стороны Java, и ScrollableResults вместе с setFetchSize() для управления загрузкой. Или, что еще лучше, сделайте свою жизнь проще и напишите сохраненную процедуру.

Так что, если вы принадлежите к этим 70% пользователей Oracle, у вас должна появиться возможность сделать вашу программу более быстрой в части отправки ответов и более масштабируемой, почти не сделав при этом никакой работы, используя только этот Один Простой Трюк.

ПОСТСКРИПТУМ: Заметьте, что если вы попытаетесь искать "JDBC fetch size" в Гугле, вы найдете кучу неправильной информации. Единственный источник, где все написано правильно, принадлежит (совпадение?) Франку Пашоту.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Комментарии (0)