Oracle: разница между deterministic и result_cache
От переводчика: свой путь на Habr я решил начать не с попытки написать какой-то уникальный текст с нуля, а с перевода относительно свежей (от 17.08.2020) статьи классика PL/SQL-разработки Стивена Фойерштайна, в которой он достаточно подробно рассматривает разницу между двумя основными вариантами кэша результатов выполнения PL/SQL функций. Надеюсь, что этот перевод будет полезен для многих разработчиков, начинающих работу с технологиями Oracle.
Введение
Рано или поздно к любому опытному разработчику Oracle обращаются с вопросом наподобие:
Я не понимаю, в чем именно разница между deterministic и result_cache. Они имеют разные сценарии использования? Я использую deterministic во многих функциях, получающих данные из справочных таблиц. Необходимо ли мне использовать ключевое слово result_cache вместо deterministic?
Я подумал, что стоит написать про различия этих двух возможностей. Прежде всего, давайте убедимся, что мы все одинаково понимаем, в каких случаях функция является детерминированной (deterministic).
В Википедии дается следующее определение детерминированного алгоритма:
Детерминированный алгоритм — это алгоритм, который возвращает один и тот же набор выходных значений для одного и того же набора входных значений, при этом осуществляя одну и ту же последовательность действий.
Иначе говоря, детерминированная подпрограмма (процедура или функция) не имеет побочных эффектов. Передавая определенный набор значений в качестве входных параметров, на выходе вы всегда получите один и тот же результат, причем вне зависимости от того, когда, где или как часто вы будете вызывать эту подпрограмму.
Резонный вопрос — что же может считаться побочным эффектом для функции на PL/SQL? Как минимум (список не является исчерпывающим):
любой (то есть совсем любой) DML-оператор
Использование переменной, объявленной вне этой функции (то есть глобальной, out-of-scope aka «global»)
вызов любой не-детерминированной подпрограммы
Далее мы кратко рассмотрим, как использовать deterministic и result_cache при разработке, после чего рассмотрим отличия этих двух возможностей и сценарии их использования. В данной статье мы будем рассматривать функции. Процедуры тоже могут быть детерминированными (но не result_cache), хотя такие процедуры, насколько мне известно, используются достаточно редко.
Создаем детерминированную функцию
FUNCTION betwnstr (
string_in IN VARCHAR2
, start_in IN INTEGER
, end_in IN INTEGER
)
RETURN VARCHAR2 DETERMINISTIC
IS
BEGIN
RETURN (SUBSTR (string_in, start_in, end_in - start_in + 1));
END;
Эта функция — простейшая обертка над substr
, позволяющая указывать начальную и конечную позицию возвращаемой подстроки, вместо начальной позиции и длины. Я думаю, что все согласны, что эта функция является детерминированной.
Для того, чтобы движок Oracle Database считал ее таковой, необходимо просто добавить ключевое слово DETERMINISTIC
к объявлению этой функции (или процедуры).
Что это даст?
Это позволяет использовать эту функцию для создания индекса, основанного на функции
Это может (но не обязано) улучшить производительность за счет кэширования и переиспользования результата выполнения этой функции
Давайте посмотрим, как кэширование влияет на детерминированную функцию:
CREATE OR REPLACE FUNCTION pass_number (i NUMBER)
RETURN NUMBER
DETERMINISTIC
IS
BEGIN
DBMS_OUTPUT.put_line ('pass_number executed');
RETURN 0;
END;
/
DECLARE
n NUMBER := 0;
BEGIN
FOR rec IN (SELECT pass_number (1)
FROM all_objects
WHERE ROWNUM < 6)
LOOP
n := n + 1;
END LOOP;
DBMS_OUTPUT.put_line (n + 1);
END;
/
pass_number executed
6
Обратите внимание, что, хотя функция была вызвана 5 раз, она была исполнена только один раз. Oracle Database создала короткоживущий кэш, действительный только для этой функции и только в этом вызове (блоке PL/SQL или SQL-запросе, из которого мы обратились к функции).
Намного более детальный разбор поведения и производительности детерминированных функций можно найти у Роба ван Вейка.
Создаем функцию, использующую result_cache
Давайте изменим уже знакомую нам функцию betwnstr
, чтобы она превратилась в использующую result_cache:
FUNCTION betwnstr (
string_in IN VARCHAR2
, start_in IN INTEGER
, end_in IN INTEGER
)
RETURN VARCHAR2
RESULT_CACHE
IS
BEGIN
RETURN (SUBSTR (string_in, start_in, end_in - start_in + 1));
END;
Все очень просто — я просто добавил ключевое слово RESULT_CACHE
. Обратите внимание, что я убрал DETERMINISTIC
, но это было сделано исключительно для читаемости. Функция может быть одновременно и детерминированной, и использующей result_cache.
Что же дает использование result_cache? Данное ключевое слово:
указывает Oracle Database, что необходимо использовать память в SGA (Shared Global Area) для кэширования входных и выходных результатов этой функции
таким образом, при вызове функции она будет исполнена лишь в том случае, если отсутствует кэшированный результат для данных входных параметров
иначе (при «попадании» в кэш этого набора входных значений) результат будет просто получен из кэша и возвращен в вызывающий контекст
если функция зависит от (в терминах Oracle — ссылается на, references) любых таблиц базы данных, то при любом commit в эти таблицы закэшированные значения функции будут автоматически удаляться
Необходимо отметить, что это лишь верхушка айсберга. RESULT_CACHE
— это опция заметно «круче» DETERMINISTIC
и может оказать заметно большее воздействие (как положительное, так и отрицательное) на производительность системы в целом. Если вы хотите использовать RESULT_CACHE
, то начать следует с подробной официальной документации. Пару примеров использования RESULT_CACHE
можно найти на Oracle Live SQL.
Давайте посмотрим, как выглядит кэширование для функций, использующих RESULT_CACHE
:
CREATE OR REPLACE FUNCTION pass_number (i NUMBER)
RETURN NUMBER
RESULT_CACHE
IS
BEGIN
DBMS_OUTPUT.put_line ('pass_number executed for ' || i);
RETURN 0;
END;
/
DECLARE
n NUMBER := 0;
BEGIN
FOR rec IN (SELECT pass_number (100)
FROM all_objects
WHERE ROWNUM < 6)
LOOP
n := n + 1;
END LOOP;
DBMS_OUTPUT.put_line ('All done ' || TO_CHAR (n + 1));
END;
/
pass_number executed for 100
All done 6
BEGIN
DBMS_OUTPUT.PUT_LINE ('Returned ' || pass_number (100));
DBMS_OUTPUT.PUT_LINE ('Returned ' || pass_number (200));
DBMS_OUTPUT.PUT_LINE ('Returned ' || pass_number (300));
DBMS_OUTPUT.PUT_LINE ('Returned ' || pass_number (100));
DBMS_OUTPUT.PUT_LINE ('Returned ' || pass_number (200));
DBMS_OUTPUT.PUT_LINE ('Returned ' || pass_number (300));
END;
/
Returned 0
pass_number executed for 200
Returned 0
pass_number executed for 300
Returned 0
Returned 0
Returned 0
Returned 0
Хотя я вызвал эту функцию трижды со входным значением 100 (один раз в первом блоке и дважды во втором), она исполнилась лишь однажды, при самом первом вызове, хотя сами вызовы относились к разным блокам исполняемого кода.
Аналогично и со входными значениями 200 и 300 — функция была выполнена лишь однажды для каждого из них, хотя я вызывал ее дважды.
Более того! Если я создам новую сессию (без перекомпиляции функции) и затем повторно вызову эти же два блока PL/SQL кода, в выводе я увижу только:
All done 6
Returned 0
Returned 0
Returned 0
Returned 0
Returned 0
Returned 0
Кэш для функций, объявленных с использованием ключевого слова RESULT_CACHE
сохраняется для различных блоков, сессий, даже для различных пользователей. Как следствие, использование этой функции может повлечь за собой цепную реакцию — положительную или отрицательную — во всей вашей системе.
Нужно помнить: если неосторожно использовать функции, использующие result_cache, то можно получить ворох непредусмотренных проблем, некоторые из которых описаны в этой статье.
Что объединяет deterministic и result_cache?
Использование ключевых слов DETERMINISTIC
и RESULT_CACHE
может улучшить производительность за счет исключения исполнения функции.
Чем они отличаются?
Кэш, создаваемый при вызове функции, объявленной с использованием ключевого слова DETERMINISTIC
, имеет узкую область видимости (только одна сессия) и небольшое время жизни (кэш существует только во время исполнения SQL-запроса, вызвавшего функцию). Общее влияние на производительность, вероятно, будет незначительным.
Наоборот, результаты исполнения, добавленные в кэш для функций, объявленных с использованием RESULT_CACHE
, доступны для всех пользователей экземпляра (instance) и существуют до тех пор, пока не инвалидированы (изменением таблицы, от которой зависит функция) или не удалены вручную. Такой вариант кэширования может намного сильнее улучшить производительность в целом, но также вынуждает мириться с риском отрицательных последствий.
Когда использовать детерминированные функции?
Можно вывести простое правило: следует добавлять ключевое слово DETERMINISTIC
в объявление любой функции, которая действительно является детерминированной.
Этим вы даете понять движкам PL/SQL и SQL, как они могут оптимизировать выполнение функции, а также делаете ее доступной для использования в некоторых специфических случаях (например, для построения основанного на функции индекса).
Даже если это не даст немедленного положительного эффекта, он может проявиться в будущем. Соответственно, было бы неплохо добавить это ключевое слово в объявление функции, если бы не одно НО.
Ни в коем случае нельзя использовать ключевое слово DETERMINISTIC
в функциях, которые не являются детерминированными. Иногда Oracle поймет, что мы его обманываем, но зачастую это приведет к проблемам во всей вашей системе.
Когда использовать функции, объявленные с использованием result_cache?
Ответить на этот вопрос сложнее. Добавление ключевого слова RESULT_CACHE
породит цепную реакцию в рамках всего экземпляра СУБД и повлияет на производительность в целом. Необходимо предметно проработать с DBA, что экземпляры разработки, тестирования и эксплуатации корректно настроены. Прежде всего, что объем SGA достаточен для всех кэшей и сделать все, чтобы избежать конфликта защелок (latch contention).
Необходимо крайне осмотрительно выбирать функции, которые будут использовать result_cache. Можно использовать следующие основные критерии для их определения:
эта функция часто вызывается с одними и теми же входными значениями?
если функция зависит от таблицы, неизменны ли данные в этой таблице? Если да, то это хороший кандидат
если функция зависит от таблицы, верно ли, что данные читаются заметно чаще, чем изменяются? Нужно помнить, что кэш будет очищен при изменении значений в таблице, от которой зависит функция
зависит ли функция от каких-либо специфических для сессии параметров, например NLS? Если да, то кэшированное значение может оказаться неверным, как, например, при использовании функции
TO_CHAR
без явно указанной маски формата. Решением в подобных случаях может стать вынос всех определяемых в сессии зависимостей в список входных параметров
Главное помнить: любая детерминированная функция является хорошим кандидатом для использования ключевого слова RESULT_CACHE
, но не всякая функция, использующая это ключевое слово, является детерминированной.