Як я можу змусити скалярну ОДС оцінюватись лише один раз у запиті?


12

У мене є запит, який повинен відфільтрувати результат скалярної АДС. Запит повинен бути надісланий як одне твердження (тому я не можу призначити результат UDF локальній змінній), і я не можу використовувати TVF. Мені відомо про проблеми з ефективністю, спричинені скалярними АДС, які включають примушування послідовно виконувати весь план, надмірні обсяги пам’яті, проблеми з оцінкою кардинальності та відсутність вбудовування. Для цього питання припустіть, що мені потрібно використовувати скалярний АДС.

Сам UDF є досить дорогим для виклику, але теоретично запити можуть бути логічно реалізовані оптимізатором таким чином, що функцію потрібно обчислити лише один раз. Я знущався з дуже спрощеного прикладу цього питання. Наступний запит на моїй машині виконує 6152 мс:

SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();

Оператор фільтра в плані запитів припускає, що функцію оцінювали один раз для кожного рядка:

план запиту 1

DDL та підготовка даних:

CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
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;

GO

DROP TABLE IF EXISTS dbo.X_100_INTEGERS;

CREATE TABLE dbo.X_100_INTEGERS (ID INT NOT NULL);

-- insert 100 integers from 1 - 100
WITH
    L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
    L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO dbo.X_100_INTEGERS WITH (TABLOCK)
SELECT n FROM Nums WHERE n <= 100;

Ось посилання на db-скрипку для наведеного вище прикладу, хоча для виконання коду потрібно близько 18 секунд.

У деяких випадках я не можу редагувати код функції, оскільки він постачається постачальником. В інших випадках я в змозі внести зміни. Як я можу змусити скалярну ОДС оцінюватись лише один раз у запиті?

Відповіді:


17

Зрештою, неможливо змусити 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();

У плані запитів більше немає оператора фільтра:

план запиту 1

Щоб бути впевненим, що він виконується лише один раз, ми можемо використовувати новий 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додано, щоб позбутися оператора фільтра. План виглядає багатообіцяючим:

план запиту 2

Виходячи з цього, можна було б очікувати, що UDF буде оцінена один раз і буде використана як зовнішня таблиця в вкладеному циклі з'єднання. Однак для запиту на моїй машині працює 6446 мс. Відповідно до sys.dm_exec_function_statsфункції виконувались 100 разів. Як це можливо? У розділі " Обчислення скалярів, виразів та виконання плану виконання " Пол Уайт вказує, що оператор "Обчислювальний скаляр" може бути відкладений:

Частіше за все обчислювальний скаляр просто визначає вираз; власне обчислення відкладається, поки щось не пізніше в плані виконання не потребує результату.

Для цього запиту схоже, що виклик UDF був відкладений до тих пір, поки він не знадобився, і в цей момент його оцінювали 100 разів.

Цікаво, що приклад CTE виконується за 71 мс на моїй машині, коли UDF не визначено з SCHEMABINDING, як в оригінальному запитанні. Функція виконується лише один раз при запуску запиту. Ось план запитів щодо цього:

план запиту 3

Не ясно, чому обчислювальний скаляр не відкладається. Це може бути через те, що недетермінізм функції обмежує перестановку операторів, яку може зробити оптимізатор запитів.

Альтернативний підхід - додати невелику таблицю до 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в основний запит . Це, мабуть, відбувається :

план запиту 4

Запит виконується приблизно в 110 мс на моїй машині, і UDF оцінюється лише один раз відповідно до sys.dm_exec_function_stats. Неправильно було б сказати, що оптимізатор запитів змушений оцінювати UDF лише один раз. Однак важко уявити перезапис оптимізатора, який би призвів до зниження запиту витрат, навіть з обмеженнями навколо UDF та обчислення скалярних витрат.

Підсумовуючи це, для детермінованих функцій (які повинні містити SCHEMABINDINGопцію) спробуйте написати запит якомога простішим способом. Якщо на SQL Server 2016 або пізнішої версії, переконайтесь, що функція виконувалася лише один раз за допомогою sys.dm_exec_function_stats. У цьому плані плани виконання можуть бути оманливими.

Для функцій, які не вважаються SQL Server детермінованими, включаючи все, що не має SCHEMABINDINGможливості, одним із підходів є розміщення UDF у ретельно складеному CTE або отриманій таблиці. Це вимагає певного догляду, але однаковий CTE може працювати як для детермінованих, так і недетермінованих функцій.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.