永続的な計算列のインデックスには、計算式の列を取得するためのキー検索が必要です


24

私は単純に連結された列で構成されているテーブルに永続的な計算列を持っています、例えば

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

これCompは一意ではなく、Dはの各組み合わせの日付から有効A, B, Cであるため、次のクエリを使用してそれぞれの終了日を取得しますA, B, C(基本的にCompの同じ値の次の開始日)を取得します。

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

次に、このクエリ(およびその他)を支援するために、計算列にインデックスを追加しました。

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

しかし、クエリプランには驚きました。私はそれを示すwhere句を持っているので、D IS NOT NULLソートしていると思っていたでしょうCompので、インデックスの外側の列を参照していないので、計算列のインデックスを使用してt1とt2をスキャンできると思っていましたが、クラスタ化インデックスを見ましたスキャン。

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

そこで、このインデックスを使用して、より良い計画が得られるかどうかを確認しました。

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

この計画を与えた

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

これは、キールックアップが使用されていることを示しています。詳細は次のとおりです。

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

さて、SQL-Serverのドキュメントによると:

列がCREATE TABLEまたはALTER TABLEステートメントでPERSISTEDとマークされている場合、確定的であるが不正確な式で定義された計算列にインデックスを作成できます。つまり、データベースエンジンは計算された値をテーブルに格納し、計算された列が依存する他の列が更新されたときに更新されます。データベースエンジンは、列にインデックスを作成するとき、およびインデックスがクエリで参照されるときに、これらの永続化された値を使用します。このオプションを使用すると、データベースエンジンが計算列式を返す関数、特に.NET Frameworkで作成されたCLR関数が確定的かつ正確であるかどうかを正確に証明できない場合に、計算列にインデックスを作成できます。

したがって、ドキュメントで「データベースエンジンが計算された値をテーブルに格納する」と言っており、その値がインデックスにも格納されている場合、A、B、Cが参照されていないときにキー検索が必要なのはなぜですかクエリはまったく?Compの計算に使用されていると思いますが、なぜですか?また、なぜクエリは上のインデックスを使用することができますt2が、ない上、t1

SQL FiddleでのクエリとDDL

NBこれは私の主な問題が発生しているバージョンであるため、SQL Server 2008にタグを付けましたが、2012年にも同じ動作をします。

回答:


20

クエリでまったく参照されていない場合、A、B、Cを取得するためにキールックアップが必要なのはなぜですか?Compの計算に使用されていると思いますが、なぜですか?

A, B, and C 、クエリプランで参照されます-それらはシークで使用されT2ます。

また、クエリがt1ではなくt2でインデックスを使用できるのはなぜですか?

オプティマイザーは、クラスター化インデックスのスキャンは、フィルター処理された非クラスター化インデックスのスキャンよりも安価であると判断し、検索を実行して列A、B、およびCの値を取得しました。

説明

本当の問題は、オプティマイザーがインデックスシークのためにA、B、Cを取得する必要性を感じた理由です。Comp非クラスター化インデックススキャンを使用して列を読み取り、同じインデックス(エイリアスT2)でシークを実行して、トップ1レコードを見つけることが期待されます。

クエリオプティマイザーは、最適化が開始される前に計算列参照を展開し、さまざまなクエリプランのコストを評価する機会を与えます。一部のクエリでは、計算列の定義を拡張すると、オプティマイザーはより効率的なプランを見つけることができます。

オプティマイザーは、相関サブクエリに遭遇すると、推論しやすい形式に「展開」しようとします。より効果的な単純化が見つからない場合は、相関サブクエリを適用(相関結合)として書き直すことになります。

書き換えを適用

この適用の展開により、論理クエリツリーが、プロジェクトの正規化(特に、一般的な式を計算列に一致させるように見える後の段階)でうまく機能しないフォームになります。

あなたの場合、クエリの記述方法はオプティマイザーの内部の詳細とやり取りするため、展開された式の定義は計算列に戻されず、計算列ではA, B, and Cなく列を参照するシークになりますComp。これが根本的な原因です。

回避策

この副作用を回避する1つのアイデアは、クエリを適用として手動で記述することです。

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

残念ながら、このクエリではフィルタ処理されたインデックスは使用されません。Dapply rejects内の列の不等式テストNULLs。そのため、明らかに冗長な述語WHERE T1.D IS NOT NULLが最適化されます。

その明示的な述語がないと、フィルター選択されたインデックス一致ロジックは、フィルター選択されたインデックスを使用できないと判断します。この2番目の副作用を回避する方法はいくつかありますが、最も簡単な方法は、相互適用を外部適用に変更することです(相関サブクエリで以前に実行されたオプティマイザーの書き換えロジックをミラーリングする)。

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

現在、オプティマイザーはアプライリライト自体を使用する必要がないため(計算列のマッチングは期待どおりに機能します)、述部も最適化されないため、フィルターインデックスは両方のデータアクセス操作に使用でき、シークはComp列を使用します両側に:

外部適用計画

これは通常、A、B、およびCを追加するよりも優先されます。 INCLUDEd、問題の根本的な原因に対処し、不必要にインデックスを広げる必要がないため、フィルタリングされたインデックスの列ます。

永続的な計算列

補足としてPERSISTEDCHECK制約でその定義を繰り返して構わないのであれば、計算列をとしてマークする必要はありません。

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

計算列はPERSISTEDNOT NULL制約を使用する場合、または制約でComp列を直接参照する(定義を繰り返すのではなく)場合にのみ必要ですCHECK


2
+1ところで、興味のある(またはそうではない)ことを確認しているときに、余分なルックアップの別のケースに遭遇しました。SQLフィドル
マーティンスミス

@MartinSmithはい、それは面白いです。別の一般的なルールの書き換え(FOJNtoLSJNandLASJN)により、期待どおりに動作しなくなり、一部の種類のプラン(カーソルなど)で有用だがここでは不要なジャンク(BaseRow / Checksums)が残ります。
ポールホワイトはGoFundMonicaを言う

ああChkはチェックサムです!おかげで私はそれについて確信が持てませんでした。もともと、それはチェック制約と関係があるのではないかと考えていました。
マーティンスミス

6

これはテストデータの人為的な性質のため、多少の偶然の一致かもしれませんが、SQL 2012で述べたように書き直しました:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

これにより、インデックスを使用し、他のオプションよりも大幅に低い読み取りで(およびテストデータで同じ結果が得られる)、優れた低コストの計画が得られました。

プランエクスプローラーには、4つのオプションの費用がかかります。オリジナル。 ヒント付きのオリジナル。 外部適用およびリード

実際のデータはより複雑であると思われるため、このクエリがあなたとは意味的に異なる動作をするシナリオがあるかもしれませんが、新しい機能が実際の違いを生むことがあることを示しています。

さらにさまざまなデータを試してみたところ、一致するシナリオとそうでないシナリオが見つかりました。

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'

1
まあそれはインデックスを使用しますが、ポイントまでしか使用しません。compが計算列でない場合、並べ替えは表示されません。
マーティンスミス

ありがとう。私の実際のシナリオはそれほど複雑ではなく、LEAD機能は2012 Expressのローカルインスタンスで希望どおりに機能しました。残念ながら、このちょっとした不便さは、実稼働サーバーをアップグレードする十分な理由とはまだ見なされていません
でした...-GarethD

-1

同じアクションを実行しようとすると、別の結果が得られました。まず、インデックスなしのテーブルの実行プランは次のようになります。ここに画像の説明を入力してください

クラスター化インデックススキャン(t2)からわかるように、(条件のため)返される必要な行を決定するために述語が使用されます。

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

インデックスがWITH演算子で定義されているかどうかに関係なく、インデックスが追加されたとき、実行計画は次のようになりました。

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

ご覧のとおり、クラスター化インデックススキャンはインデックススキャンに置き換えられています。上で見たように、SQL Serverは計算列のソース列を使用して、ネストされたクエリの照合を実行します。クラスター化インデックススキャン中に、これらすべての値を同時に取得できます(追加の操作は不要です)。インデックスが追加されたとき、テーブルからの必要な行のフィルタリング(メイン選択で)はインデックスに従って実行されますが、計算列のソース列の値をcomp取得する必要があります(最後の操作のネストされたループ) 。

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

このため、計算された列のソース列のデータを取得するためにキー検索操作が使用されます。

PSはSQL Serverのバグのように見えます。

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