web-dev-qa-db-ja.com

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

単純に連結された列で構成されるテーブルに永続的な計算列があります。

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;

しかし、クエリプランには驚きました。そのD IS NOT NULLを示すwhere句があり、Compで並べ替えており、計算列のインデックスを使用してインデックスの外部にある列を参照していないため、 t1とt2をスキャンしましたが、クラスター化インデックススキャンが見つかりました。

enter image description here

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

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;

これはこの計画を与えた

enter image description here

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

enter image description here

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

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

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

SQL FiddleのクエリとDDL

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

24
GarethD

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

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

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

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

説明

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

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

オプティマイザーが相関サブクエリに遭遇すると、推論しやすいフォームに「アンロール」しようとします。より効果的な簡略化が見つからない場合は、相関サブクエリを適用(相関結合)として書き換えます。

Apply rewrite

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

あなたのケースでは、クエリの記述方法がオプティマイザの内部の詳細と相互に作用するため、拡張された式の定義は計算された列と照合されず、最終的にはA, B, and Cの列を参照するシークになります。計算列、Comp。これが根本的な原因です。

Workaround

この副作用を回避する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;

残念ながら、このクエリでは、希望どおりにフィルタリングされたインデックスを使用しません。適用内の列Dの不等式テストは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を使用します両側の列:

Outer Apply Plan

これは、問題の根本原因に対処し、インデックスを不必要に拡大する必要がないため、A、B、およびCをフィルター選択されたインデックスのINCLUDEd列として追加するよりも一般的に推奨されます。

永続的な計算列

補足として、PERSISTED制約でその定義を繰り返すことを気にしない場合は、計算列をCHECKとしてマークする必要はありません。

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;

この場合、NOT NULL制約を使用するか、PERSISTED制約でComp列を直接(その定義を繰り返すのではなく)参照する場合、計算列はCHECKである必要があります。

20
Paul White 9

テストデータの人為的な性質により、これは少し偶然の一致かもしれませんが、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;

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

Plan Explorer costs for four options: Original; original with hint; outer apply and Lead

実際のデータはより複雑であるため、このクエリの動作がセマンティックに異なるシナリオがいくつかあるかもしれませんが、新しい機能が実際に違いをもたらす場合があることを示しています。

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

--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'
6
wBob