単純に連結された列で構成されるテーブルに永続的な計算列があります。
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をスキャンしましたが、クラスター化インデックススキャンが見つかりました。
したがって、このインデックスを使用して、より良い計画が得られるかどうかを確認するように強制しました。
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
でインデックスを使用できるのはなぜですか?
N.B。これは私の主な問題が発生しているバージョンであるため、SQL Server 2008にタグを付けましたが、2012年にも同じ動作が得られます。
クエリで参照されていないのに、A、B、Cを取得するためにキー検索が必要なのはなぜですか?Compの計算に使用されていると思いますが、なぜですか?
列A, B, and C
areクエリプランで参照されます-それらはT2
のシークで使用されます。
また、クエリはなぜt2ではインデックスを使用できますが、t1では使用できないのですか?
オプティマイザーは、クラスター化インデックスをスキャンする方が、フィルター処理された非クラスター化インデックスをスキャンしてからルックアップを実行して列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;
残念ながら、このクエリでは、希望どおりにフィルタリングされたインデックスを使用しません。適用内の列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
を使用します両側の列:
これは、問題の根本原因に対処し、インデックスを不必要に拡大する必要がないため、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
である必要があります。
テストデータの人為的な性質により、これは少し偶然の一致かもしれませんが、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;
これにより、インデックスを使用し、他のオプションよりも読み取りが大幅に少ないニースの低コスト計画が得られました(テストデータでも同じ結果)。
実際のデータはより複雑であるため、このクエリの動作がセマンティックに異なるシナリオがいくつかあるかもしれませんが、新しい機能が実際に違いをもたらす場合があることを示しています。
さらに多様なデータを試してみたところ、一致するシナリオとそうでないシナリオが見つかりました。
--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'