web-dev-qa-db-ja.com

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

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

query plan with literal value

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

query plan with parameter value

私は計画を間違って読んでいた可能性があります。パフォーマンスの問題には気づきませんでしたが、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;
8
binki

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

SQL Serverは、反復として末尾再帰共通テーブル式(CTE)を書き換えます。 Lazy Index Spoolからのすべてが、反復変換のランタイム実装です。 answer から 再帰的な共通テーブル式でのEXCEPTの使用 で、実行プランのこのセクションがどのように機能するかについての詳細な説明を書きました。

述語(フィルター)outsideをCTEに指定し、クエリオプティマイザーを使用する場合は、このフィルターを押し下げます再帰(反復として書き換え)内で、アンカーメンバーに適用します。これは、再帰が_ParentId = @Id_に一致するレコードのみから始まることを意味します。

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

再帰的なCTEで述語をプッシュするためのルールはSelOnIterator-再帰を実装する反復子でのリレーショナル選択(=述語)と呼ばれます。より正確には、このルールは選択範囲を再帰反復のanchor部分にコピーできます。

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

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

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

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

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

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

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

12
Paul White 9

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

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

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

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

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

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

_@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;
_

トレースフラグ4199を使用したSQL Server 2014 SP3のクエリプラン

トレースフラグ4199を使用したSQL Server 2012 SP4 GDRのクエリプラン

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

SQL Server 2016より前のバージョンを使用する場合、トレースフラグ4199をグローバルに有効にすることが主なコンセンサスです。その後、それを有効にするかどうかについて話し合うことができます。そのQ-A here


互換性レベル130または140

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

enter image description here

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


OPTION(RECOMPILE)

プロシージャが使用されている場合でも、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
_

enter image description here

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

10
Randi Vertongen