クエリでスカラーUDFを一度だけ評価するにはどうすればよいですか?


12

スカラーUDFの結果に対してフィルタリングする必要があるクエリがあります。クエリは単一のステートメントとして送信する必要があるため(UDF結果をローカル変数に割り当てることができません)、TVFを使用できません。スカラーUDFによって引き起こされるパフォーマンスの問題を認識しています。これには、計画全体を連続的に実行すること、過剰なメモリ許可、カーディナリティー推定の問題、インライン化の欠如が含まれます。この質問については、スカラーUDFを使用する必要があると想定してください。

UDF自体は呼び出すのにかなり費用がかかりますが、理論的には、関数を一度計算するだけで済むように、オプティマイザーによってクエリを論理的に実装できます。この質問の非常に単純化された例をモックアップしました。次のクエリは、マシンで実行するのに6152ミリ秒かかります。

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

クエリプランのフィルター演算子は、関数が行ごとに1回評価されたことを示しています。

クエリプラン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;

ここでデシベルフィドルリンクのコードが実行するのに約18秒かかりますが、上記の例のために。

場合によっては、ベンダーから提供されているため、関数のコードを編集できないことがあります。それ以外の場合は、変更を加えることができます。クエリでスカラーUDFを一度だけ評価するにはどうすればよいですか?

回答:


17

最終的に、クエリ内でSQL ServerにスカラーUDFを一度だけ評価させることはできません。ただし、それを促進するために実行できるいくつかの手順があります。テストでは、現在のバージョンのSQL Serverで機能するものを入手できると思いますが、将来の変更のためにコードを再確認する必要がある可能性があります。

コードを編集することが可能な場合、最初に試してみるのは、可能であれば関数を決定論的にすることです。Paul White は、オプションを使用して関数を作成する必要があり、関数コード自体が決定論的でなければならないことをここで指摘します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

SQL Server 2016でリリースされた新しいsys.dm_exec_function_stats DMVを使用できるように1回だけ実行されることを確認するには:

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');

ALTER関数に対してを発行すると、execution_countそのオブジェクトのがリセットされます。上記のクエリは1を返します。これは、関数が一度だけ実行されたことを意味します。

関数が決定論的であるという理由だけで、クエリに対して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が1回評価され、ネストされたループ結合の外部テーブルとして使用されることが予想されます。ただし、クエリを実行するには6446ミリ秒かかります。sys.dm_exec_function_stats関数によると、100回実行されました。それはどのように可能ですか?「スカラーの計算、式、および実行計画のパフォーマンス」で、Paul Whiteは、スカラーの計算演算子を延期できることを指摘しています。

多くの場合、Compute Scalarは単に式を定義します。実際の計算は、実行計画の後半で結果が必要になるまで延期されます。

このクエリの場合、UDF呼び出しは必要になるまで延期されたように見え、その時点で100回評価されました。

興味深いことに、SCHEMABINDING元の質問のように、UDFがで定義されていない場合、CTEの例は私のマシンで71ミリ秒で実行されます。この関数は、クエリの実行時に1回だけ実行されます。以下がそのためのクエリプランです。

クエリプラン3

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行を返します。いずれにせよ、UDFが決定論的でない場合、オプティマイザーはCTEが1行を返すことを保証できないため、結合の前にUDFが評価される可能性が高いようです。オプティマイザーがスキャンしdbo.X_ONE_ROW_TABLE、ストリーム集計を使用して、返された1行の最大値を取得し(関数を評価する必要があります)、それをdbo.X_100_INTEGERSメインクエリのネストされたループ結合の外部テーブルとして使用することを期待します。これは何が起こるかのようです:

クエリプラン4

クエリは私のマシンで約110ミリ秒で実行され、UDFはに従って1回だけ評価されsys.dm_exec_function_statsます。クエリオプティマイザーがUDFを一度だけ評価することを強制されると言うのは誤りです。ただし、UDFと計算スカラーコストに関する制限がある場合でも、低コストのクエリにつながるオプティマイザーの書き換えを想像するのは困難です。

要約すると、決定性関数(SCHEMABINDINGオプションを含める必要がある)については、できるだけ単純な方法でクエリを記述してください。SQL Server 2016以降のバージョンでは、を使用して関数が1回だけ実行されたことを確認しsys.dm_exec_function_statsます。その点で実行計画は誤解を招く可能性があります。

SCHEMABINDINGオプションが欠けているものを含め、SQL Serverによって決定論的と見なされない関数の場合、UDFを慎重に作成されたCTEまたは派生テーブルに配置する方法があります。これには少し注意が必要ですが、同じCTEが確定的機能と非確定的機能の両方で機能します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.