web-dev-qa-db-ja.com

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

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

UDF自体を呼び出すにはかなりのコストがかかりますが、理論的には、クエリをオプティマイザによって論理的に実装して、関数を1回だけ計算するだけで済みます。この質問の非常に単純化した例をモックアップしました。次のクエリを私のマシンで実行するには6152ミリ秒かかります。

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

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

query plan 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 fiddle link ですが、コードの実行には約18秒かかります。

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

12
Joe Obbish

結局のところ、SQL Serverにクエリで一度だけスカラーUDFを評価させることはできません。ただし、それを奨励するために実行できるいくつかの手順があります。テストを行うと、SQL Serverの現在のバージョンで動作するものを入手できると思いますが、将来の変更により、コードの再検討が必要になる可能性があります。

コードを編集することが可能な場合、最初に試すことは、可能であれば関数を確定的にすることです。 Paul Whiteは here は、関数は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();

クエリプランにはフィルター演算子がなくなりました。

query plan 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が追加され、フィルター演算子が取り除かれました。計画は有望に見えます:

query plan 2

これに基づいて、UDFが1回評価され、ネストされたループ結合で外部テーブルとして使用されることが期待されます。ただし、私のマシンでクエリを実行するには6446ミリ秒かかります。 sys.dm_exec_function_statsによると、関数は100回実行されました。それはどのように可能ですか? " Compute Scalars、Expressions and Execution Plan Performance "で、Paul WhiteはCompute Scalar演算子を延期できることを指摘しています。

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

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

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

query plan 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に。これは どうなる のようです:

query plan 4

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

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

SCHEMABINDINGオプションが不足しているものを含め、SQL Serverによって確定的ではないと見なされる関数の1つのアプローチは、UDFを注意深く作成されたCTEまたは派生テーブルに配置することです。これには少し注意が必要ですが、同じCTEが確定的関数と非確定的関数の両方で機能します。

17
Joe Obbish