パラメータ付きのこの再帰的CTEがリテラルで行うとき、なぜインデックスを使用しないのですか?


8

ツリー構造で再帰CTEを使用して、ツリー内の特定のノードのすべての子孫をリストしています。WHERE句にリテラルノード値を書き込むと、SQL Serverは実際にその値にのみCTEを適用し、実際の行数少ないクエリプランなどを提供します。

リテラル値を持つクエリプラン

ただし、値をパラメーターとして渡すと、CTE実現(スプール)され、事実の後でフィルター処理されるようです。

パラメータ値を持つクエリプラン

私は計画を間違って読んでいた可能性があります。パフォーマンスの問題には気づきませんでしたが、CTEの実現により、特にビジーなシステムでは、より大きなデータセットで問題が発生する可能性があると心配しています。また、私は通常、このトラバーサルをそれ自体で複合します。祖先までトラバースし、子孫まで戻ります(すべての関連ノードを確実に収集するため)。私のデータが原因で、「関連」ノードの各セットはかなり小さいため、CTEの実現は意味がありません。また、SQL ServerがCTEを実現しているように見える場合、その「実際の」数には非常に多くの数値が含まれています。

クエリのパラメーター化されたバージョンをリテラルバージョンのように機能させる方法はありますか?CTEを再利用可能なビューにしたいと考えています。

リテラルを使用したクエリ:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

パラメータ付きのクエリ:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

セットアップコード:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;

回答:


12

Randi Vertongenの答えは、クエリのパラメーター化されたバージョンを使用して必要な計画を取得する方法を正しく扱っています。この回答は、詳細に興味がある場合に備えて、質問のタイトルに対処することで補足しています。

SQL Serverは、反復として末尾再帰共通テーブル式(CTE)を書き換えます。レイジーインデックススプールからすべては、反復変換のランタイム実装です。私は、実行計画のこのセクションはどのように働くかの詳細な説明を書いた答え再帰共通テーブル式を除いて使用します

CTEの外部で述語(フィルター)を指定し、クエリオプティマイザーがこのフィルターを再帰(反復として書き換えられます)の内部にプッシュし、アンカーメンバーに適用するようにします。これは、一致するレコードのみから再帰が始まることを意味しますParentId = @Id

これは、リテラル値、変数、またはパラメーターが使用されているかどうかにかかわらず、かなり合理的な期待です。ただし、オプティマイザは、ルールが記述されているものに対してのみ実行できます。ルールは、特定の変換を実現するために論理クエリツリーを変更する方法を指定します。これらには、最終結果が安全であることを確認するロジックが含まれています。つまり、可能なすべてのケースで、元のクエリ仕様とまったく同じデータを返します。

再帰CTEで述語をプッシュするためのルールが呼び出されますSelOnIterator-再帰を実装する反復子でのリレーショナル選択(=述語)。より正確には、このルールは選択を再帰反復のアンカー部分にコピーできます。

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

このルールは、文書化されていないヒントで無効にすることができますOPTION(QUERYRULEOFF SelOnIterator)。これを使用すると、オプティマイザーはリテラル値を持つ述部を再帰CTEのアンカーにプッシュできなくなります。あなたはそれを望まないが、それは要点を説明している。

当初、このルールはリテラル値のみの述語での作業に限定されていました。また、を指定することにより、変数またはパラメーターを操作することもできます。OPTION (RECOMPILE)そのヒントにより、パラメーターの埋め込みの最適化が有効になり、変数(またはパラメーター)のランタイムリテラル値がプランのコンパイル時に使用されます。プランはキャッシュされないため、この欠点は、実行ごとに新しいコンパイルが行われることです。

ある時点で、SelOnIteratorルールは変数とパラメーターでも機能するように改善されました。予期しない計画の変更を回避するために、これは4199トレースフラグ、データベース互換性レベル、およびクエリオプティマイザーホットフィックス互換性レベルで保護されていました。これは、常に文書化されているわけではない、オプティマイザの改善のための非常に正常なパターンです。通常、改善はほとんどの人にとって良いことですが、変更によって誰かに退行が生じる可能性は常にあります。

CTEを再利用可能なビューにしたい

ビューの代わりにインラインテーブル値関数を使用できます。プッシュダウンする値をパラメーターとして指定し、述部を再帰アンカーメンバーに配置します。

必要に応じて、トレースフラグ4199をグローバルに有効にすることもオプションです。このフラグでカバーされる多くのオプティマイザの変更があるため、有効にしてワークロードを慎重にテストし、リグレッションを処理する準備をする必要があります。


10

現在のところ、実際の修正プログラムのタイトルはわかりませんが、ご使用のバージョン(SQL Server 2012)でクエリオプティマイザーの修正プログラムを有効にすると、より優れたクエリプランが使用されます。

他のいくつかの方法は次のとおりです。

  • OPTION(RECOMPILE)フィルタリングを使用すると、リテラル値でフィルタリングが早く行われます。
  • SQL Server 2016以降では、このバージョンより前の修正プログラムが自動的に適用され、クエリはより優れた実行プランと同等に実行されるはずです。

クエリオプティマイザーの修正プログラム

あなたはこれらの修正を有効にすることができます

  • SQL Server 2016より前のトレースフラグ4199
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; SQL Server 2016以降(修正には必要ありません)

フィルタリング@idは、ホットフィックスが有効になっている実行プランの再帰メンバーとアンカーメンバーの両方に以前に適用されます。

トレースフラグはクエリレベルで追加できます。

OPTION(QUERYTRACEON 4199)

SQL Server 2012 SP4 GDRまたはSQL Server 2014 SP3でTraceflag 4199を使用してクエリを実行する場合、より適切なクエリプランが選択されます。

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

SQL Server 2014 SP3でのクエリプラン、トレースフラグ4199

SQL Server 2012 SP4 GDRでのクエリプランとトレースフラグ4199

SQL Server 2012 SP4 GDRでのクエリプラン(トレースフラグ4199なし)

SQL Server 2016より前のバージョンを使用する場合、主なコンセンサスはtraceflag 4199をグローバルに有効にすることです。その後、有効にするかどうかの議論のために開かれています。その上AQ / A ここに


互換性レベル130または140

compatibility_level= 130または140のデータベースでパラメータ化されたクエリをテストする場合、フィルタリングはより早く行われます。

ここに画像の説明を入力してください

SQL Server 2016以降では、トレースフラグ4199の「古い」修正が有効になっているためです。


オプション(再コンパイル)

プロシージャが使用されている場合でも、SQL Serverはを追加するときにリテラル値でフィルタリングできますOPTION(RECOMPILE);

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

ここに画像の説明を入力してください

OPTION(RECOMPILE)を使用したSQL Server 2012 SP4 GDRのクエリプラン

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