В конечном счете, невозможно заставить SQL Server вычислять скалярную UDF только один раз в запросе. Тем не менее, есть некоторые шаги, которые могут быть предприняты для его поощрения. Я считаю, что с тестированием вы можете получить что-то, что работает с текущей версией SQL Server, но возможно, что в будущих изменениях вам потребуется пересмотреть свой код.
Если есть возможность отредактировать код, первое, что нужно попробовать, - это сделать функцию детерминированной, если это возможно. Пол Уайт указывает здесь , что функция должна быть создана с помощью SCHEMABINDING
опции и сам код функции должен быть детерминированным.
После внесения следующих изменений:
CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE @tbl TABLE (VAL VARCHAR(5));
-- make the function expensive to call
INSERT INTO @tbl
SELECT [VALUE]
FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');
RETURN 1;
END;
Запрос из вопроса выполняется за 64 мс:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
План запроса больше не имеет оператора фильтра:
Чтобы убедиться, что он выполняется только один раз, мы можем использовать новый DMV sys.dm_exec_function_stats, выпущенный в SQL Server 2016:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
Вызов ALTER
функции против сбрасывает execution_count
для этого объекта. Приведенный выше запрос возвращает 1, что означает, что функция была выполнена только один раз.
Обратите внимание, что только то, что функция является детерминированной, не означает, что она будет оценена только один раз для любого запроса. Фактически, для некоторых запросов добавление SCHEMABINDING
может ухудшить производительность. Рассмотрим следующий запрос:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Лишнее DISTINCT
было добавлено, чтобы избавиться от оператора Filter. План выглядит многообещающим:
Исходя из этого, можно ожидать, что UDF будет оценен один раз и будет использоваться в качестве внешней таблицы в соединении с вложенным циклом. Однако для выполнения запроса на моей машине требуется 6446 мс. Согласно sys.dm_exec_function_stats
функции была выполнена 100 раз. Как это возможно? В статье «Производительность вычисления скаляров, выражений и плана выполнения » Пол Уайт указывает, что оператор вычисления скаляров может быть отложен:
Чаще всего Compute Scalar просто определяет выражение; фактическое вычисление откладывается до тех пор, пока что-то позже в плане выполнения не потребует результата.
Для этого запроса похоже, что вызов UDF был отложен до тех пор, пока он не был необходим, после чего он был оценен 100 раз.
Интересно, что пример CTE выполняется на моей машине за 71 мс, когда UDF не определен SCHEMABINDING
, как в исходном вопросе. Функция выполняется только один раз при выполнении запроса. Вот план запроса для этого:
Непонятно, почему Compute Scalar не откладывается. Это может быть связано с тем, что недетерминизм функции ограничивает перестановку операторов, которую может выполнять оптимизатор запросов.
Альтернативный подход - добавить небольшую таблицу в CTE и запросить единственную строку в этой таблице. Подойдет любая маленькая таблица, но давайте использовать следующее:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
Запрос тогда становится:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Добавление dbo.X_ONE_ROW_TABLE
добавляет неопределенности для оптимизатора. Если в таблице ноль строк, CTE вернет 0 строк. В любом случае оптимизатор не может гарантировать, что CTE вернет одну строку, если UDF не является детерминированным, поэтому кажется вероятным, что UDF будет оцениваться до объединения. Я ожидал бы, что оптимизатор будет сканировать dbo.X_ONE_ROW_TABLE
, использовать агрегат потока, чтобы получить максимальное значение одной возвращенной строки (что требует оценки функции), и использовать это в качестве внешней таблицы для присоединения к вложенному циклу dbo.X_100_INTEGERS
в основном запросе. , Похоже, это то, что происходит :
Запрос выполняется примерно за 110 мс на моем компьютере, и UDF оценивается только один раз согласно sys.dm_exec_function_stats
. Было бы неправильно говорить, что оптимизатор запросов вынужден оценивать UDF только один раз. Однако трудно представить переписывание оптимизатора, которое привело бы к снижению затрат на запросы, даже с учетом ограничений, связанных с UDF и вычислением скалярных затрат.
Таким образом, для детерминированных функций (которые должны включать эту SCHEMABINDING
опцию) попробуйте написать запрос как можно более простым способом. Если на SQL Server 2016 или более поздней версии, подтвердите, что функция была выполнена только один раз, используя sys.dm_exec_function_stats
. Планы выполнения могут вводить в заблуждение в этом отношении.
Для функций, которые SQL Server не считает детерминированными, включая те, которые не имеют SCHEMABINDING
опций, один из подходов заключается в том, чтобы поместить UDF в тщательно сконструированный CTE или производную таблицу. Это требует небольшой осторожности, но один и тот же CTE может работать как для детерминированных, так и для недетерминированных функций.