インデックスまたはテーブルスキャンを使用する場合、SQL Serverは統計を使用してどちらが優れているかを確認します。
2,000万行のテーブルがあります。 (SnapshotKey、Measure)のインデックスと次のクエリがあります。
select Measure, SnapshotKey, MeasureBand
from t1
where Measure = 'FinanceFICOScore'
group by Measure, SnapshotKey, MeasureBand
クエリは500k行を返します。したがって、クエリはテーブルの行の2.5%のみを選択します。
問題は、SQL Serverが私が持っている非クラスター化インデックスを使用せず、代わりにテーブルスキャンを使用する理由です。
統計が更新されます。
ただし、クエリのパフォーマンスは良好です。
CREATE TABLE [t1](
[SnapshotKey] [int] NOT NULL,
[SnapshotDt] [date] NOT NULL,
[Measure] [nvarchar](30) NOT NULL,
[MeasureBand] [nvarchar](30) NOT NULL,
-- and many more fields
) ON [PRIMARY]
データウェアハウスであるため、テーブルにPKはありません。
CREATE NONCLUSTERED INDEX [nci_SnapshotKeyMeasure] ON [t1]
(
[SnapshotKey] ASC,
[Measure] ASC
)
多くの行を返す場合や、行が非常に広い場合は、インデックスシークが最良の選択ではない可能性があります。インデックスがカバーしていない場合、ルックアップは高価になる可能性があります。 ここの#2を参照 。
シナリオでは、クエリオプティマイザーは、50,000回の個別ルックアップを実行すると、単一のスキャンよりもコストがかかると推定しています。スキャンとシークの間のオプティマイザの選択(クエリで必要な列のRIDルックアップを使用するが、非クラスタ化インデックスには存在しない)は、各代替の推定コストに基づいています。
オプティマイザは常に、検討する最低コストの選択肢を選択します。 2つの実行プランのルートノードのEstimated Subtree Costプロパティを見ると、スキャンプランの見積もりコストがシークプランよりも低いことがわかります。その結果、オプティマイザはスキャンを選択しました。それは本質的にあなたの質問への答えです。
現在、オプティマイザが使用するコストモデルは、システムのパフォーマンス特性と一致する可能性が低い仮定と「マジックナンバー」に基づいています。特に、モデルで行われた1つの仮定は、必要なデータまたはインデックスページが既にメモリ内にない状態でクエリが実行を開始することです。もう1つは、シーケンシャルI/O(スキャンに期待される)が、RIDルックアップで想定されるランダムI/Oパターンよりも安価であることです。他にも多くのそのような仮定と警告があり、ここで詳細に説明するには多すぎます。
それにもかかわらず、コストモデル全体は、ほとんどのデータベーススキーマ、ほとんどのハードウェア構成、ほとんどの時間で、ほとんどのクエリに対して一般に「十分な」計画を生成することが示されています。どこにでも。考えてみれば、それはかなりの成果です。
モデルの制限やその他の要因により、オプティマイザが実際には「十分に十分」ではない計画を選択する場合があります。あなたは「パフォーマンスが良い」と報告しているので、ここではそうではないようです。
実際には、一致する行が595,947行あり、これはデータの約3%です。したがって、ルックアップのコストはすぐに加算されます。テーブルに1ページあたり100行あるとします。これは、テーブルスキャンで読み取る200,000ページです。これは、595,947回のルックアップよりもはるかに安価です。
とともに GROUP BY
句の質問では、複合キー(Measure、SnapshotKey、MeasureBand)をオンにした方がよいでしょう。
「ミッシングインデックス」の提案をご覧ください。ルックアップを回避するために列を含めるように指示します。より一般的には、クエリで他の列を参照する場合、それらは新しいインデックスのキーまたはINCLUDE
句にある必要があります。それ以外の場合は、これらの値を取得するために595,947回のルックアップを実行する必要があります。
たとえば、クエリの場合:
select Measure, SnapshotKey, MeasureBand, SUM(NumLoans), SUM(PrinBal)
from t1
where Measure = 'FinanceFICOScore'
group by Measure, SnapshotKey, MeasureBand
...必要になります:
CREATE INDEX ixWhatever
ON t1 (Measure, SnapshotKey, MeasureBand)
INCLUDE (NumLoans,PrinBal);
WHERE条件のフィールドは、インデックスの先行フィールドではありません。
measure
がNVARCHARとして定義されているため、リテラルの前にN
:where Measure = N'FinanceFICOScore'
を付けます。
SnapshotKey
でクラスター化インデックスを作成することを検討してください。一意の場合は、PK(およびクラスター化)にすることができます。一意でない場合は、PKにすることはできませんが、一意でないクラスタ化インデックスにすることはできます。その場合、非クラスタ化インデックスはmeasure
列にのみ存在します。
また、GROUP BY
の最初のフィールドもmeasure
であることを考えると、measure
を先頭フィールドにすることにもメリットがあります。
実際、この操作では、代わりにMeasure, SnapshotKey, MeasureBand
にNonClustered Indexを定義する必要があります。これは、GROUP BY
句と一致する正確な順序で行います。サイズに関しては、実際にはMeasureBand
が実際に追加されているだけです。NonClusteredインデックスはすでにMeasure
に基づいており、MeasureKey
はすでにインデックスに含まれているため、クラスター化インデックスキーです(Measure
はNonClusteredインデックスに複製されません)。
@Robは、この問題を解決するためにこれらの3つのフィールドでこの順序で非クラスター化インデックスを定義することだけが必要であり、SnapshotKey
にクラスター化(非一意)インデックスを作成する必要がないことを、彼の回答の削除済みコメントで言及していました- 必要。彼はおそらく正しいですが(フィールドの数が少なくなることを望んでいました)、クラスタ化インデックスを使用すると、この操作だけでなく、他のほとんどの場合にも有益であると私は主張します。