15 Занятие 15: Оптимизация запросов / Функции
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: qpt_15_optim_functions.html Archive: QPT-handouts-16.zip Страниц PDF: 26
Page 1: Оптимизация запросов / Функции
Авторские права
© Postgres Professional, 2019–2024 Авторы: Егор Рогов, Павел Лузанов, Павел Толмачев, Илья Баштанов Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Категории изменчивости
- Подстановка кода функции в текст запроса
- Вызов табличных функций
- Настройки COST и ROWS
- Вспомогательные функции планировщика
- Конфигурационные параметры
Page 3: Категории изменчивости
Volatile
- возвращаемое значение может произвольно меняться
- при одинаковых значениях входных параметров
- используется по умолчанию
- Stable
- значение не меняется в пределах одного оператора SQL
- функция не может менять состояние базы данных
- Immutable
- значение не меняется, функция детерминирована
- функция не может менять состояние базы данных
Комментарии преподавателя
Каждой функции сопоставлена категория изменчивости, которая определяет свойства возвращаемого значения при одинаковых значениях входных параметров.
Категория Volatile говорит о том, что возвращаемое значение может произвольно меняться. Такие функции будут выполняться каждый раз при каждом вызове. Если при создании функции категория не указана, назначается именно эта категория.
Категория Stable используется для функций, возвращаемое значение которых не меняется в пределах одного SQL-оператора. В частности, такие функции не могут менять состояние БД. Такая функция может быть выполнена один раз во время выполнения запроса, а затем будет использоваться вычисленное значение.
Категория Immutable еще более строгая: возвращаемое значение не меняется никогда. Такую функцию можно выполнить на этапе планирования запроса, а не во время выполнения.
Можно — не означает, что всегда происходит именно так, но планировщик вправе выполнить такие оптимизации.
https://postgrespro.ru/docs/postgresql/16/xfunc-volatility
Page 4: Категории изменчивости и оптимизация
Благодаря дополнительной информации о поведении функции, которую дает указание категории изменчивости, оптимизатор может сэкономить на вызовах функции. Для экспериментов создадим функцию, возвращающую случайное число:
Проверим план выполнения следующего запроса:
В плане мы видим обращение к табличной функции generate_series в узле Function Scan. Ее результат фильтруется, для каждой строки в фильтре честно вызывается скалярная функция random. В этом можно убедиться и воочию:
Page 5: Page 5
Здесь случайным образом получаем от 0 до 10 строк. Функция с категорией изменчивости Stable будет вызвана всего один раз — поскольку мы фактически указали, что ее значение не может измениться в пределах оператора:
Узел Result формирует строку выборки, а выражение One-Time Filter вычисляется один раз, так что результатом запроса будет либо 0, либо 10 строк.
Наконец, категория Immutable позволяет вычислить значение функции еще на этапе планирования, поэтому во время выполнения вычисление условия фильтра уже не требуется:
Page 6: Page 6
Для Immutable получаем случайный план! Ответственность «за дачу заведомо ложных показаний» лежит на разработчике.
Page 7: Подстановка кода функций
Скалярные функции на SQL
- один оператор SELECT без предложения FROM,
- возвращает одно значение
- вызываемые функции не должны быть изменчивее вызывающей
- и др.
- Табличные функции на SQL
- один оператор SELECT
- категория Immutable или Stable
- функция не STRICT
- и др.
Комментарии преподавателя
Оптимизатор PostgreSQL умеет подставлять (inline) тело функции в SQL-запрос. Это работает как со скалярными, так и с табличными функциями.
В обоих случаях есть много ограничений: функция должна быть написана на языке SQL, использовать единственный оператор SELECT и т. д. Скалярная функция, к тому же, не должна обращаться к таблицам базы данных и вызывать функции, имеющие менее строгую категорию, а табличная — быть стабильной или постоянной.
Важным преимуществом подстановки тела функции в запрос является то, что функция становится прозрачной для планировщика. Например, дополнительные условия в теле основного запроса могут быть применены к запросу из тела функции, что позволит как можно раньше отфильтровать лишние строки.
https://wiki.postgresql.org/wiki/Inlining\_of\_SQL\_functions
Page 8: Подстановка кода функций в SQL-запрос
Тело очень простых скалярных функций на языке SQL может быть подставлено прямо в основной SQL-оператор на этапе разбора запроса. В этом случае время на вызов функции не тратится. Пример мы уже видели: наша функция rnd(). Проверим, какой будет план запроса в случае, когда категория изменчивости функции rnd (Stable) не соответствует категории изменчивости функции random (Volatile):
В фильтре упоминается функция rnd(). Поменяем категорию изменчивости функции на Volatile:
Теперь в фильтре упоминается функция random(), но не rnd(). Она будет вызываться напрямую, минуя «обертку» в виде функции rnd(). Возможностей для подстановки табличных функций гораздо больше. Например, в таких функциях допускаются обращения к таблицам.
При подстановке табличные функции работают наподобие представлений с параметрами. Планировщик оптимизирует весь запрос, функция прозрачна для него:
Page 9: Page 9
Page 10: Вызов табличных функций
{#source-figure-010 caption=“Source slide page 10: Вызов табличных функций”}
Комментарии преподавателя
Когда табличная функция вызывается в предложении FROM, за ее вычисление отвечает узел Function Scan. При этом сначала все строки, возвращаемые функцией, материализуются, и только затем передаются родительскому узлу плана. Таково текущее ограничение реализации.
Если же функция вызывается в предложении SELECT, в плане она выполняется в узле ProjectSet. В этом случае функция может воспользоваться интерфейсом возвращения строк «по требованию» (value-per-call), без материализации.
Так работают функции на языке SQL, большинство встроенных функций (написанных на С) и функции на PL/Python, возвращающие итератор. Функции на других языках программирования тоже могут использовать этот интерфейс, если такая возможность в них реализована.
Page 11: Табличные функции
Вызовем функцию generate_series в предложении FROM с ограничением на количество строк (LIMIT):
В плане запроса видим узел Function Scan — сначала были получены все строки из функции, и только потом наложено ограничение LIMIT. А теперь повторим запрос, только вызовем функцию из предложения SELECT:
Теперь в плане появился узел ProjectSet, который формирует десять строк выборки — в данном случае оптимизатору удалось получать строки по требованию. Время выполнения запроса сократилось на порядки. Узел Result здесь представляет опущенное в запросе предложение FROM — он передает родительскому узлу ровно одну строку. Однако не все функции могут возвращать строки по одной. Например, обращение к любой функции на языке PL/pgSQL возвращает все строки результата:
Вызовем функцию из предложения SELECT:
В плане — узел ProjectSet, но теперь серверу приходится получить результат функции полностью, а затем наложить ограничение LIMIT. Время выполнения это хорошо показывает. В предложении SELECT может быть несколько вызовов табличных функций, а сами функции могут вкладываться друг в друга:
Page 12: Page 12
Нижний узел ProjectSet формирует выборку из результатов выполнения двух табличных функций: generate_series(1,3) и unnest. В этой выборке оказываются три строки. Верхний узел ProjectSet формирует итоговую выборку, вычисляя внешний вызов generate_series. Без учета материализации такой запрос эквивалентен следующему запросу, в котором функции вызываются в предложении FROM:
Page 13: Настройки COST и ROWS
CREATE FUNCTION fun( )
- Function Scan ( rows=1000 cost=100 )
- оценки
- по умолчанию
- CREATE FUNCTION fun( ) ROWS 12 COST 123
- Function Scan ( rows=12 cost=123 )
- ROWS
- COST
Комментарии преподавателя
Обычно (если не удалось подставить тело функции в запрос) оптимизатор не имеет возможности анализировать код функции и воспринимает ее как «черный ящик».
Однако можно дать оптимизатору приблизительную информацию о стоимости вызова функции и числе возвращаемых строк.
Параметр COST задает стоимость пользовательской функции в единицах cpu_operator_cost. По умолчанию функции на C получают оценку 1, а на других языках — 100.
Параметр ROWS указывает примерное число возвращаемых строк.
COST и ROWS можно указывать как при создании функции, так и для уже существующих функций.
https://postgrespro.ru/docs/enterprise/16/sql-createfunction
Page 14: Настройки COST и ROWS
Напишем табличную функцию на языке PL/pgSQL, выводящую дни недели:
План запроса:
Стоимость выполнения функции считается постоянной, для пользовательских функций по умолчанию она равна стоимости 100 операторов:
Но это значение можно изменить:
В плане изменилась начальная стоимость. Сервер оценивает кардинальность результата этой функции как 1000, хотя фактическое значение равно семи. С помощью указания ROWS можно подсказать серверу ориентировочное количество строк, которое вернет функция:
Повторим запрос:
Page 15: Page 15
Теперь сервер считает, что функция вернет 10 строк, поэтому уменьшилась полная стоимость узла:
Измененные значения можно увидеть в системном каталоге:
Или с помощью метакоманды psql:
Page 16: Вспомогательные функции
{#source-figure-016 caption=“Source slide page 16: Вспомогательные функции”}
CREATE FUNCTION fun(x) ROWS 18
- CREATE FUNCTION fun(x) SUPPORT fun_support
Комментарии преподавателя
Параметры COST и ROWS позволяют задать стоимость и количество строк, возвращаемых функцией, как постоянные величины. Но константы не всегда дают желаемый результат.
В PostgreSQL есть возможность для функции написать вспомогательную функцию, которая предоставляет планировщику информацию, зависящую от значений аргументов основной (целевой) функции.
Вспомогательная функция может по значениям аргументов целевой функции выдавать:
● оценку ее стоимости;
● оценку числа возвращаемых строк;
● выражение, эквивалентное вызову функции.
Дополнительно для функций, возвращающих boolean:
● оценку селективности;
● эквивалентный предикат с индексируемым оператором.
Вспомогательная функция должна быть написана на языке С.
https://postgrespro.ru/docs/postgresql/16/xfunc-optimization
Page 17: Вспомогательные функции планировщика
Посмотрим план запроса с вызовом функции generate_series:
В отличии от функции days_of_week, оптимизатор сразу правильно оценивает количество возвращаемых строк. Более того, оценка зависит от параметров функции:
Благодаря вспомогательной функции (она может быть написана только на языке С) планировщик получает дополнительную информацию, которую использует для вычисления селективности условий, кардинальности функции или ее стоимости. Посмотреть, имеется ли вспомогательная функция, можно в таблице pg_proc:
Как видно, вспомогательные функции существуют не для всех перегруженных вариантов функции generate_series. Посмотрим на план запроса, генерирующего ряд дат:
Без вспомогательной функции и указания ROWS оптимизатор не имеет информации о числе строк в результате и поэтому использует значение по умолчанию (1000). С каждой версией в PostgreSQL появляются новые вспомогательные функции.
Page 18: Пометки параллельности
CREATE FUNCTION fun( ) PARALLEL …
- небезопасные для распараллеливания
- (по умолчанию)
UNSAFE
- ограниченно распараллеливаемые
- RESTRICTED
- безопасные для распараллеливания
- SAFE
Комментарии преподавателя
В теме «Параллельная обработка» было рассказано про то, что не каждый запрос может выполняться в параллельном режиме. Поскольку оптимизатор не может проанализировать тело функции, он рассчитывает на пометки параллельности, определяя по ним возможность параллельной обработки.
При создании функции (или позже) можно указать одну из трех пометок:
● UNSAFE — запрещаются параллельные планы, если в запросе есть вызов функции;
● RESTRICTED — разрешаются параллельные планы, но запрещено вызывать функцию в параллельной части плана;
● SAFE — безопасна для параллельной обработки.
По умолчанию используется пометка UNSAFE.
Пометки параллельности указываются также для пользовательских агрегатных функций.
https://postgrespro.ru/docs/postgresql/16/parallel-safety#PARALLEL-LABELI NG
Page 19: Пометки параллельности
Пометки параллельности можно увидеть в столбце proparallel таблицы pg_proc (r=restricted, s=safe, u=unsafe):
Все основные стандартные функции безопасны. Пометки также показывает метакоманда \df+ утилиты psql (поле Parallel):
Проверим, как пометка параллельности влияет на план выполнения запроса. Напишем функцию, вычисляющую стоимость билета. Она помечена как безопасная для параллельного выполнения:
Запрос проверяет, что общая стоимость бронирований совпадает с общей стоимостью билетов:
Page 20: Page 20
План запроса состоит из двух частей: в узле InitPlan 1 выполняется подзапрос с агрегацией по tickets, а подзапрос в узле InitPlan 2 выполняет агрегацию по bookings. Сейчас оба подзапроса выполняются параллельно. Поменяем пометку параллельности на UNSAFE:
Теперь оба подзапроса выполняются последовательно — пометка запрещает параллельные планы. А теперь пометим функцию как ограниченно распараллеливаемую (RESTRICTED):
Подзапрос с функцией выполняется последовательно ведущим процессом, для второго подзапроса выбран параллельный план.
Конфигурационные параметры
В ряде случаев может оказаться удобным оформить запросы в виде хранимых подпрограмм (например, с целью предоставить к ним доступ приложению). В этом случае дополнительным преимуществом может быть возможность
Page 21: Page 21
установки параметров для конкретных подпрограмм. Рассмотрим в качестве примера запрос:
Допустим, мы хотим использовать параллельные планы, но именно этот запрос собираемся выполнять последовательно. Тогда мы можем установить параметр на уровне функции:
О том, как проверить план запроса, выполняющегося внутри функции, мы говорили в теме «Профилирование». Воспользуемся расширением auto_explain:
Выполним запрос:
Выведем последние строки журнала сообщений:
Page 22: Итоги
Функция — черный ящик для планировщика, если ее тело не подставляется в запрос
- Вызов табличных функций обычно материализуется
- Планировщику можно дать дополнительную информацию
- категорию изменчивости функции
- кардинальность и стоимость
- пометку параллельности
- Вспомогательные функции помогают оптимизировать вызовы встроенных функций
Page 23: Практика
- Отключите материализацию общего табличного выражения, в котором вызывается функция random. Объясните результат. 2. Напишите функцию-обертку на SQL для запроса SELECT * FROM generate_series (1, 10_000_000). Рассмотрите три варианта: сам исходный запрос и вызовы функции в предложениях FROM и SELECT. Сравните планы выполнения запросов и использование временных файлов. Что меняется при установке категории изменчивости Stable? 3. Какую категорию изменчивости имеет функция days_of_week из демонстрации? Какова ее изменчивость на самом деле?
- Определение функции:
Page 24: 1. Материализация изменчивых функций в CTE
Общее табличное выражение с изменчивой функцией всегда материализуется:
В этом случае указание NOT MATERIALIZED не действует:
2. Функция-обертка
По умолчанию функция имеет категорию изменчивости Volatile. Базовый запрос:
Поскольку функция вызывается в предложении FROM, происходит материализация. Памяти work_mem не хватает, все строки сбрасываются на диск (temp written) и затем считываются (temp read). Вызов функции в предложении SELECT:
Page 25: Page 25
В узле ProjectSet нет материализации, поэтому цифры temp written/read остались без изменений. Однако время выполнения сильно увеличилось: оно тратится на передачу десяти миллионов строк от узла к узлу по одной строке. Вызов функции в предложении FROM:
Здесь к узлу Function Scan внутри функции добавляется еще один в основном запросе, поэтому количество использованных временных страниц удваивается. Сменим категорию изменчивости функции:
При вызове функции из предложения SELECT ничего не меняется:
Вызовем функцию из предложения FROM:
Теперь тело функции подставляется в основной запрос.
3. Дни недели
Функция была создана следующей командой:
Категория изменчивости не указана, поэтому подразумевается Volatile. Может показаться, что это постоянная функция (Immutable), поскольку у нее нет параметров и список дней недели не меняется.
Page 26: Page 26
Однако названия дней недели зависят от настройки локализации. Текущее значение:
Изменим настройку:
Теперь функция возвращает названия дней недели на английском языке, поэтому правильным будет задать категорию Stable: