В
любом приложении, которое активно использует вызовы хранимых функций PL/SQL
внутри SQL-запросов, происходит падение производительности, связанное с
переключением контекста.
В ходе обработки блока программного кода ядро PL/SQL выполняет процедурные команды самостоятельно, а команды SQL передает ядру SQL. Уровень SQL выполняет команды и при необходимости возвращает результаты ядру PL/SQL. Передача управления между ядрами PL/SQL и SQL (см. схему выше) называется переключением контекста.
Каждое переключение контекста приводит к дополнительным затратам ресурсов. Необходимость в переключении контекста, приводящим к снижению производительности, встречается во многих ситуациях. PL/SQL и SQL тесно интегрированы на синтаксическом уровне, но во внутренней реализации они связаны отнюдь не так тесно, как кажется на первый взгляд.
Создадим таблицу и наполним ее данными:
И также создадим функцию и процедуру, которые в дальнейшем будем использовать в запросе.
-- Procedure
-- Function
Этот процесс, связанный с переключением в среду PL/SQL VM и возвратом результатов в SQL-engine называется "переключение контекста между SQL и PL/SQL" (SQL and PL/SQL context switch).
На переключение контекста расходуется дополнительные ресурсы, - прежде всего процессорное время.
В общем-то, в всех популярных книгах по оптимизации производительности в среде Oracle Database, рекомендуется избегать использования PL/SQL-вызовов внутри SQL-запросов. К сожалению, это не всегда возможно: для реализации сложных вычислений прямо в тексте запроса часто недостаточно средств только языка SQL.
1.1 PL/SQL-подпрограммы в определении SQL-запроса
До Oracle Database версии 12c приходилось мириться с потерей производительности на переключение контекста между исполняющей средой SQL и PL/SQL VM.
В версии 12c появилась возможность прямо в тексте SQL-запроса, в фразе WITH, включать определение функций которые в нем используются. Это позволяет минимизировать затраты на переключение контекста.
Попробуем сравнить время выполнения запроса написанного в двух вариантах.
💧 Вариант с PL/SQL-подпрограммами, определенными вне запроса (всего в таблице находятся 1 млн. записей):
💧 Вариант с функциями, определенными в самом тексте запроса:
Конечно, пример этот искусственный: запрос очень простой, и основные расходы
при его выполнении составляют именно затраты на переключение контекста. В
реальных приложениях выигрыш скорее всего не будет таким фантастическим, но все
равно будет значительным.
Также рекомендуется, чтобы внутри PL/SQL-подпрограмм, определенных внутри
SQL-запроса, не было сторонних PL/SQL-вызовов (вызываемые объекты, в которых
определены вне запроса).
Попробуем определить процедуру inc, вне запроса, то есть обычным образом
Продолжительность выполнения запроса увеличилась, но все равно она меньше, чем с вариантом использования PL/SQL-подпрограмм , целиком определенных внутри SQL-запроса.
Следует отметить, что при определении PL/SQL-подпрограмм внутри запроса не происходит его непосредственного выполнения в среде SQL-engine, то есть среда выполнения SQL не имеет в своем составе собственной виртуальной машины PL/SQL. Происходит генерирование дополнительной информации для SQL-компилятора, с помощью которой в ходе компиляции и выполнения запроса уменьшаются затраты на переключение контекста. Таким образом, переключение контекста, как таковое, все равно происходит, но значительно снижаются накладнhttps://observsp.blogspot.com/2021/05/merge.htmlые расходы на это переключение.
1.2 Прагма компиляции UDF для уже существующих PL/SQL-подпрограмм
Как же быть
в том случае, если уже есть работающее унаследованное приложение (разработанное
до версии Oracle Database 12c), в котором обычным образом определены
PL/SQL-функции, и эти функции используются в SQL-запросах?
Переписывать все SQL-запросы, в которых есть PL/SQL-вызовы, может быть
трудоемкой задачей. Более того, может оказаться так, что часть
PL/SQL-подпрограмм вызываются как в SQL-запросах, так и в других
PL/SQL-объектах. В этом случае переписывание запросов приведет к дублированию
кода, то есть один и тот же код нужно будет сопровождать в двух местах.
Для решения этой проблемы в Oracle Database 12c введена новая директива компилятора (прагма) которая позволяет отметить подпрограммы, которые затем будут вызываться в SQL-запросах.
-- Function
По-прежнему
существует переключение контекста, но часть работы выполняется во время
компиляции, а не во время выполнения.
При этом, даже если процедура или функция имеет эту прагму в своем объявлении, ничего не мешает вызывать ее в PL/SQL:
begin
Интересно,
изменится ли время выполнения PL/SQL-функции внутри PL/SQL-блока, если ее
пометить с помощью прагмы UDF?
💧 Вариант c функциями, определенными с прагмой компилятора UDF:
💧 Вариант c функциями без прагмы компилятора UDF.
Закомментируем PRAGMA UDF в функции и процедуре и выполним тот же блок:
Время выполнения вызовов PL/SQL-процедур, объявленных с директивой компиляции UDF, больше примерно на семь процентов.
Замедление вызовов небольшое, но все же не стоит устанавливать эту директиву компиляции в всех PL/SQL-подпрограммах - это нужно делать только в тех процедурах, которые будут интенсивно вызываться в SQL-запросах.
1.3 Использование PL/SQL-подпрограмм в подзапросах
Есть одна важная особенность, связанная с использованием PL/SQL вызовов в подзапросах - если использовать определение PL/SQL в подзапросе, то немедленно получим ошибку:
function inc_amount(v_pValue in number) RETURN number is
1.4 Проблема с использованием детерминированных функций в запросах
Особое внимание следует обратить на использование недерминированных функций в запросе. Внутри запроса функция становится недетерминированной, даже несмотря на то, что опция DETERMINISTIC была указана в ее определении.
Для удобства очистим таблицу и вставим в нее 6 записей: 1 строку с неповторяющимися знаениями и продублируем 5 строк.
Теперь попробуем функцию inc_amount объявить детерминированной:
При этом неважно, было ли тело функции явно включено в текст запроса, либо мы воспользовались директивой компиляции UDF:
Функция перестает быть детерминированной внутри SQL-запроса
Если функция "тяжелая", то есть при выполнении дает большую вычислительную нагрузку, то приходится использовать старый синтаксис, с значительными затратами на переключение контекста:
В 18с это уже устранено. Результат детерминированной функции в 18-й версии будет таким же, как и со старым синтаксисом.
1.5 Использование внутри запросов функций с кэшем результатов
Хорошая новость заключается в том что для существующих функций, использующих кэш результатов (RESULT_CACHE), их встраивание в запрос через директиву компиляции UDF замечательно работает:
Но для функций, определение которых непосредственно входит в запрос, ошибка возникает еще на этапе компиляции:
Будем надеяться, что в следующих версиях (или патчсетах) СУБД эта проблема тоже будет устранена. В 18с еще не устранили.
Заключение
Встраивание PL/SQL-хранимых процедур непосредственно в текст запросов представляет собой очень сильный инструмент повышения производительности SQL-запросов использующих вызовы PL/SQL.
Директива компиляции UDF (pragma UDF) позволяет с минимальной
модификацией кода приложения использовать эту новую технологию.
При использовании встраивания PL/SQL-подпрограмм в определение запроса следует
обратить внимание на детерминированные (deterministic) PL/SQL-функции. В
настоящий момент детерминированные функции при переносе их в тело запроса (явно
или неявно, с помощью директивы UDF) теряют это свойство.
Мнение автора статьи:
На мой взгляд первое, что должен сделать разработчик после перехода на Oracle Database 12c - это вставить прагму UDF в все недетерминированные PL/SQL-функции, которые используются в SQL-запросах! :-)
Данный пост почти целиком взят из блога Oracle Database 12c: PL/SQL inside SQL query - Life in Oracle - Блог Игоря Мельникова.
0 Комментарии