このことに出くわしたとき、私は何か他のものを研究していました。いくつかのデータを含むテストテーブルを生成し、さまざまなクエリを実行して、クエリを記述するさまざまな方法が実行プランにどのように影響するかを調べました。以下は、ランダムなテストデータを生成するために使用したスクリプトです。
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID('t') AND type in (N'U'))
DROP TABLE t
GO
CREATE TABLE t
(
c1 int IDENTITY(1,1) NOT NULL
,c2 int NULL
)
GO
insert into t
select top 1000000 a from
(select t1.number*2048 + t2.number a, newid() b
from [master]..spt_values t1
cross join [master]..spt_values t2
where t1.[type] = 'P' and t2.[type] = 'P') a
order by b
GO
update t set c2 = null
where c2 < 2048 * 2048 / 10
GO
CREATE CLUSTERED INDEX pk ON [t] (c1)
GO
CREATE NONCLUSTERED INDEX i ON t (c2)
GO
ここで、このデータを指定して、次のクエリを呼び出しました。
select *
from t
where
c2 < 1048576
or c2 is null
;
驚いたことに、このクエリに対して生成された実行プランは this でした。 (申し訳ありませんが、外部リンクは大きすぎてここに収まりません)。
誰かがこれらすべての " 定数スキャン "と " コンピュートスカラー "の問題を説明してくれませんか?何が起こっていますか?
|--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1010], [Expr1011], [Expr1012]))
|--Merge Interval
| |--Sort(TOP 2, ORDER BY:([Expr1013] DESC, [Expr1014] ASC, [Expr1010] ASC, [Expr1015] DESC))
| |--Compute Scalar(DEFINE:([Expr1013]=((4)&[Expr1012]) = (4) AND NULL = [Expr1010], [Expr1014]=(4)&[Expr1012], [Expr1015]=(16)&[Expr1012]))
| |--Concatenation
| |--Compute Scalar(DEFINE:([Expr1005]=NULL, [Expr1006]=NULL, [Expr1004]=(60)))
| | |--Constant Scan
| |--Compute Scalar(DEFINE:([Expr1008]=NULL, [Expr1009]=(1048576), [Expr1007]=(10)))
| |--Constant Scan
|--Index Seek(OBJECT:([t].[i]), SEEK:([t].[c2] > [Expr1010] AND [t].[c2] < [Expr1011]) ORDERED FORWARD)
定数スキャンはそれぞれ、列のない単一のメモリ内行を生成します。一番上の計算スカラーは、3列の単一行を出力します
Expr1005 Expr1006 Expr1004
----------- ----------- -----------
NULL NULL 60
下の計算スカラーは、3つの列を持つ単一の行を出力します
Expr1008 Expr1009 Expr1007
----------- ----------- -----------
NULL 1048576 10
連結演算子は、これらの2つの行を結合して3つの列を出力しますが、現在は名前が変更されています
Expr1010 Expr1011 Expr1012
----------- ----------- -----------
NULL NULL 60
NULL 1048576 10
Expr1012
列はフラグのセットです ストレージエンジンの特定のシークプロパティを定義するために内部的に使用されます 。
出力に沿った次の計算スカラー2行
Expr1010 Expr1011 Expr1012 Expr1013 Expr1014 Expr1015
----------- ----------- ----------- ----------- ----------- -----------
NULL NULL 60 True 4 16
NULL 1048576 10 False 0 0
最後の3つの列は次のように定義され、マージ間隔演算子に提示する前のソート目的でのみ使用されます
[Expr1013] = Scalar Operator(((4)&[Expr1012]) = (4) AND NULL = [Expr1010]),
[Expr1014] = Scalar Operator((4)&[Expr1012]),
[Expr1015] = Scalar Operator((16)&[Expr1012])
Expr1014
およびExpr1015
は、特定のビットがフラグでオンになっているかどうかをテストします。 Expr1013
は、4
のビットがオンで、Expr1010
がNULL
の場合、ブール列trueを返すようです。
クエリで他の比較演算子を試すと、これらの結果が得られます
+----------+----------+----------+-------------+----+----+---+---+---+---+
| Operator | Expr1010 | Expr1011 | Flags (Dec) | Flags (Bin) |
| | | | | 32 | 16 | 8 | 4 | 2 | 1 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
| > | 1048576 | NULL | 6 | 0 | 0 | 0 | 1 | 1 | 0 |
| >= | 1048576 | NULL | 22 | 0 | 1 | 0 | 1 | 1 | 0 |
| <= | NULL | 1048576 | 42 | 1 | 0 | 1 | 0 | 1 | 0 |
| < | NULL | 1048576 | 10 | 0 | 0 | 1 | 0 | 1 | 0 |
| = | 1048576 | 1048576 | 62 | 1 | 1 | 1 | 1 | 1 | 0 |
| IS NULL | NULL | NULL | 60 | 1 | 1 | 1 | 1 | 0 | 0 |
+----------+----------+----------+-------------+----+----+---+---+---+---+
ここから、ビット4は「範囲の開始点がある」ことを意味し(無制限ではない)、ビット16は範囲の開始点が包括的であることを意味すると推測します。
この6列の結果セットは、Expr1013 DESC, Expr1014 ASC, Expr1010 ASC, Expr1015 DESC
でソートされたSORT
演算子から出力されます。 True
は1
で表され、False
は0
で表されると仮定すると、以前に表された結果セットはすでにその順序になっています。
私の以前の仮定に基づくと、この種類の最終的な効果は、次の順序で範囲をマージ間隔に提示することです
ORDER BY
HasStartOfRangeAndItIsNullFirst,
HasUnboundedStartOfRangeFirst,
StartOfRange,
StartOfRangeIsInclusiveFirst
マージ間隔演算子は2行を出力します
Expr1010 Expr1011 Expr1012
----------- ----------- -----------
NULL NULL 60
NULL 1048576 10
放出された各行について、範囲シークが実行されます
Seek Keys[1]: Start:[dbo].[t].c2 > Scalar Operator([Expr1010]),
End: [dbo].[t].c2 < Scalar Operator([Expr1011])
したがって、2つのシークが実行されているように見えます。 1つは> NULL AND < NULL
で、もう1つは> NULL AND < 1048576
です。ただし、渡されたフラグは、これをそれぞれIS NULL
および< 1048576
に変更するように見えます。うまくいけば @ sqlkiwi はこれを明確にして不正確な部分を修正できるでしょう!
クエリを少し変更すると
select *
from t
where
c2 > 1048576
or c2 = 0
;
次に、複数のシーク述語を使用したインデックスシークを使用すると、計画ははるかに単純になります。
計画はSeek Keys
を示しています
Start: c2 >= 0, End: c2 <= 0,
Start: c2 > 1048576
この単純な計画がOPのケースに使用できない理由の説明は、SQLKiwiが 以前にリンクされたブログ投稿 へのコメントで提供しています。
複数の述語を持つインデックスシークは、異なるタイプの比較述語(つまり、OPの場合はIs
とEq
)を混在させることはできません。これは、製品の現在の制限にすぎません(おそらく、最後のクエリの等価テストc2 = 0
が、単純な等価ではなく>=
と<=
を使用して実装される理由です)クエリc2 = 0 OR c2 = 1048576
を取得します。
定数スキャンは、SQL Serverがバケットを作成する方法であり、実行プランの後半にバケットを配置します。私はもっと投稿しました ここでのそれの完全な説明 。常時スキャンの目的を理解するには、計画をさらに詳しく調べる必要があります。この場合、定数スキャンによって作成されたスペースを埋めるために使用されているのは、Compute Scalarオペレーターです。
Compute Scalarオペレーターには、NULLと値1045876が読み込まれているため、データをフィルター処理する目的でループ結合で使用されることは明らかです。
本当にクールな部分は、この計画が簡単なことです。それは最小限の最適化プロセスを経たことを意味します。すべての操作は、マージ間隔につながります。これは、インデックスシークのための最小限の比較演算子のセットを作成するために使用されます( 詳細はこちら )。
全体的なアイデアは、重複する値を削除して、最小限のパスでデータを引き出せるようにすることです。まだループ操作を使用していますが、ループは1回だけ実行されることに注意してください。つまり、ループは事実上スキャンです。
補遺:その最後の文はオフです。 2つのシークがありました。私はその計画を読み違えた。残りの概念は同じで、目標、最小限のパスは同じです。