web-dev-qa-db-ja.com

実行プランはINDEXを使用せず、テーブルスキャンを使用します

インデックスまたはテーブルスキャンを使用する場合、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が私が持っている非クラスター化インデックスを使用せず、代わりにテーブルスキャンを使用する理由です。

統計が更新されます。

ただし、クエリのパフォーマンスは良好です。

テーブルスキャン

Table Scan

強制インデックス

Force Index

テーブル/インデックス構造

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
)
9
user71787

多くの行を返す場合や、行が非常に広い場合は、インデックスシークが最良の選択ではない可能性があります。インデックスがカバーしていない場合、ルックアップは高価になる可能性があります。 ここの#2を参照

シナリオでは、クエリオプティマイザーは、50,000回の個別ルックアップを実行すると、単一のスキャンよりもコストがかかると推定しています。スキャンとシークの間のオプティマイザの選択(クエリで必要な列のRIDルックアップを使用するが、非クラスタ化インデックスには存在しない)は、各代替の推定コストに基づいています。

オプティマイザは常に、検討する最低コストの選択肢を選択します。 2つの実行プランのルートノードのEstimated Subtree Costプロパティを見ると、スキャンプランの見積もりコストがシークプランよりも低いことがわかります。その結果、オプティマイザはスキャンを選択しました。それは本質的にあなたの質問への答えです。

現在、オプティマイザが使用するコストモデルは、システムのパフォーマンス特性と一致する可能性が低い仮定と「マジックナンバー」に基づいています。特に、モデルで行われた1つの仮定は、必要なデータまたはインデックスページが既にメモリ内にない状態でクエリが実行を開始することです。もう1つは、シーケンシャルI/O(スキャンに期待される)が、RIDルックアップで想定されるランダムI/Oパターンよりも安価であることです。他にも多くのそのような仮定と警告があり、ここで詳細に説明するには多すぎます。

それにもかかわらず、コストモデル全体は、ほとんどのデータベーススキーマ、ほとんどのハードウェア構成、ほとんどの時間で、ほとんどのクエリに対して一般に「十分な」計画を生成することが示されています。どこにでも。考えてみれば、それはかなりの成果です。

モデルの制限やその他の要因により、オプティマイザが実際には「十分に十分」ではない計画を選択する場合があります。あなたは「パフォーマンスが良い」と報告しているので、ここではそうではないようです。

16
Aaron Bertrand

実際には、一致する行が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);
9
Rob Farley
  1. WHERE条件のフィールドは、インデックスの先行フィールドではありません。

  2. measureがNVARCHARとして定義されているため、リテラルの前にNwhere 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にクラスター化(非一意)インデックスを作成する必要がないことを、彼の回答の削除済みコメントで言及していました- 必要。彼はおそらく正しいですが(フィールドの数が少なくなることを望んでいました)、クラスタ化インデックスを使用すると、この操作だけでなく、他のほとんどの場合にも有益であると私は主張します。

6
Solomon Rutzky