Зрештою, неможливо змусити 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();
У плані запитів більше немає оператора фільтра:
Щоб бути впевненим, що він виконується лише один раз, ми можемо використовувати новий sys.dm_exec_function_stats DMV, випущений у 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
додано, щоб позбутися оператора фільтра. План виглядає багатообіцяючим:
Виходячи з цього, можна було б очікувати, що UDF буде оцінена один раз і буде використана як зовнішня таблиця в вкладеному циклі з'єднання. Однак для запиту на моїй машині працює 6446 мс. Відповідно до sys.dm_exec_function_stats
функції виконувались 100 разів. Як це можливо? У розділі " Обчислення скалярів, виразів та виконання плану виконання " Пол Уайт вказує, що оператор "Обчислювальний скаляр" може бути відкладений:
Частіше за все обчислювальний скаляр просто визначає вираз; власне обчислення відкладається, поки щось не пізніше в плані виконання не потребує результату.
Для цього запиту схоже, що виклик UDF був відкладений до тих пір, поки він не знадобився, і в цей момент його оцінювали 100 разів.
Цікаво, що приклад CTE виконується за 71 мс на моїй машині, коли UDF не визначено з SCHEMABINDING
, як в оригінальному запитанні. Функція виконується лише один раз при запуску запиту. Ось план запитів щодо цього:
Не ясно, чому обчислювальний скаляр не відкладається. Це може бути через те, що недетермінізм функції обмежує перестановку операторів, яку може зробити оптимізатор запитів.
Альтернативний підхід - додати невелику таблицю до 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 може працювати як для детермінованих, так і недетермінованих функцій.