クエリは次のとおりです。
SELECT top 100 a.LocationId, b.SearchQuery, b.SearchRank
FROM dbo.Locations a
INNER JOIN dbo.LocationCache b ON a.LocationId = b.LocationId
WHERE a.CountryId = 2
AND a.Type = 7
ロケーションインデックス:
PK_Locations:
LocationId
IX_Locations_CountryId_Type:
CountryId、タイプ
LocationCacheインデックス:
PK_LocationCache:
LocationId
IX_LocationCache_LocationId_SearchQuery_SearchRank:
LocationId、SearchQuery、SearchRank
実行計画:
つまり、ロケーションでIndex Seekを実行し、カバーするインデックスを使用します。
しかし、なぜそれはインデックスをカバーするLocationCacheでインデックススキャンを実行しているのですか?
そのカバーするインデックスには、インデックスにLocationId、SearchQuery、SearchRankがあります(「含まれる列」としてではありません)。
インデックススキャンにカーソルを合わせます。
このクエリは、オートコンプリートプラグインによって使用される、SQL Server FTSカタログによって提供されるインデックス付きビューで実行する必要があるため、100%最適化する必要があります。
上記のクエリが3秒かかっている瞬間。 <0である必要があります。
何か案は?
追加の変更が行われると、クエリのパフォーマンスが低下する可能性があることを念頭に置いて、INNER LOOP JOIN
を使用すると、dbo.LocationCache
でカバーインデックスが強制的に使用されます。
SELECT top 100 a.LocationId, b.SearchQuery, b.SearchRank
FROM dbo.Locations a
INNER LOOP JOIN dbo.LocationCache b ON a.LocationId = b.LocationId
WHERE a.CountryId = 2
AND a.Type = 7
主にマージ結合も使用しているため、インデックススキャンを使用しています。マージ結合演算子には、結合条件と互換性のある順序で両方ともソートされた2つの入力ストリームが必要です。
また、マージ結合演算子を使用して内部結合を実現しています。これは、より一般的なネストされたループ結合演算子よりも高速であると考えているためです。そして、それはおそらく正しいです(通常はそうです)。選択した2つのインデックスを使用することで、結合条件(LocationID)に従って両方とも事前にソートされた入力ストリームがあります。入力ストリームがこのように事前にソートされている場合、マージ結合はほとんどの場合、他の2つ(ループ結合とハッシュ結合)よりも高速です。
欠点はあなたが気づいたことです:それはインデックス全体をスキャンしているように見えます、それでそれが決して使われないかもしれない非常に多くのレコードを読んでいるならどうしてそれはより速くなることができますか?答えは、スキャン(シーケンシャルな性質のため)は、シークの10倍から100倍のレコード/秒をどこでも読み取ることができるということです。
現在、シークは選択的であるため、通常は勝ちます。スキャンは非選択的であるのに対し、シークは要求した行のみを取得します。範囲内のすべての行を返す必要があります。ただし、スキャンの読み取り速度ははるか高いため、破棄された行と一致する行の比率がスキャン行/秒の比率よりも低いである限り、シークを上回ることがよくありますVS.行/秒を探します。
質問?
OK、最後の文をもっと説明するように頼まれました:
「破棄された行」は、スキャンが読み取る行です(インデックス内のすべてを読み取る必要があるため)が、反対側に一致するものがないため、おそらくマージ結合演算子によって拒否されます。 WHERE句の条件はすでにそれを除外しています。
「一致する行」は、実際にマージ結合内の何かと一致する、読み取った行です。これらは、スキャンがシークに置き換えられた場合にシークによって読み取られたのと同じ行です。
クエリプランの統計を見ると、何があるかがわかります。インデックススキャンの左側にある巨大な太い矢印が表示されますか?これは、オプティマイザーがスキャンで読み取ると考える行数を表します。投稿したインデックススキャンの統計ボックスには、返された実際の行が約540万(5,394,402)であることが示されています。これは次のようになります。
TotalScanRows = (MatchingRows + DiscardedRows)
(私の言葉では、とにかく)。一致する行を取得するには、マージ結合演算子によって報告された「実際の行」を確認します(これを正確に取得するには、TOP 100を削除する必要がある場合があります)。これを知ったら、次の方法で破棄された行を取得できます。
DiscardedRows = (TotalScanRows - MatchingRows)
そして今、あなたは比率を計算することができます。
統計を更新しようとしましたか?
UPDATE STATISTICS dbo.LocationCache
これが何をするのか、そしてクエリオプティマイザがシークよりもスキャンを選択する理由についてのいくつかの良いリファレンスがあります。
概要
ここで考慮すべきことがいくつかあります。まず、SQLは、使用するのに最適な(十分な)計画を決定するときに、クエリを調べ、次に、関連するテーブルに関して格納されている統計も調べます。
次に、インデックスを検索する方が効率的か、インデックスのリーフレベル全体をスキャンする方が効率的かを判断します(この場合、クラスター化インデックスであるため、テーブル内のすべてのページにアクセスする必要があります)。多くのこと。まず、スキャンする必要のある行/ページの数を推測します。これは転換点と呼ばれ、あなたが思っているよりも低いパーセンテージです。この素晴らしいキンバリートリップのブログをご覧ください http://www.sqlskills.com/BLOGS/KIMBERLY/category/The-Tipping-Point.aspx
転換点の制限内にある場合は、統計が古くなっているか、インデックスが大幅に断片化されている可能性があります。
FORCESEEKクエリヒントを使用してSQLにインデックスを強制的にシークさせることは可能ですが、これは注意して使用してください。一般的に、すべてを維持している限り、SQLは最も効率的な計画を決定するのに非常に優れています。
つまり、LocationCacheにフィルターがないため、テーブルのコンテンツ全体が返されます。あなたは完全にカバーするインデックスを持っています。インデックススキャン(1回)は最も安価な操作であり、クエリオプティマイザがそれを選択します。
最適化するには:テーブル全体を結合し、後で上位100件の結果のみを取得します。それらの大きさはわかりませんが、[Locations]テーブルをサブクエリしてみてくださいCountryId, Type
そして、結果だけを[LocationCache]と結合します。 1000行を超える行がある場合は、高速になります。また、可能であれば、結合する前に、より制限的なフィルターを追加してみてください。
インデックススキャン:スキャンは、適格かどうかに関係なくテーブルのすべての行に影響を与えるため、コストはテーブルの行の総数に比例します。したがって、テーブルが小さい場合、またはほとんどの行が述語の対象となる場合、スキャンは効率的な戦略です。
インデックスシーク:シークは、適格な行とこれらの適格な行を含むページにのみ触れるため、コストは、テーブル内の行の総数ではなく、適格な行とページの数に比例します。
テーブルにインデックスがあり、クエリが大量のデータにアクセスしている場合、つまりクエリがデータの50%または90%以上を取得している場合、オプティマイザはすべてのデータページをスキャンして取得しますデータ行。
私は簡単なテストを行い、次のことを思いつきました
CREATE TABLE #Locations
(LocationID INT NOT NULL ,
CountryID INT NOT NULL ,
[Type] INT NOT NULL
CONSTRAINT PK_Locations
PRIMARY KEY CLUSTERED ( LocationID ASC )
)
CREATE NONCLUSTERED INDEX [LocationsIndex01] ON #Locations
(
CountryID ASC,
[Type] ASC
)
CREATE TABLE #LocationCache
(LocationID INT NOT NULL ,
SearchQuery VARCHAR(50) NULL ,
SearchRank INT NOT NULL
CONSTRAINT PK_LocationCache
PRIMARY KEY CLUSTERED ( LocationID ASC )
)
CREATE NONCLUSTERED INDEX [LocationCacheIndex01] ON #LocationCache
(
LocationID ASC,
SearchQuery ASC,
SearchRank ASC
)
INSERT INTO #Locations
SELECT 1,1,1 UNION
SELECT 2,1,4 UNION
SELECT 3,2,7 UNION
SELECT 4,2,7 UNION
SELECT 5,1,1 UNION
SELECT 6,1,4 UNION
SELECT 7,2,7 UNION
SELECT 8,2,7 --UNION
INSERT INTO #LocationCache
SELECT 4,'BlahA',10 UNION
SELECT 3,'BlahB',9 UNION
SELECT 2,'BlahC',8 UNION
SELECT 1,'BlahD',7 UNION
SELECT 8,'BlahE',6 UNION
SELECT 7,'BlahF',5 UNION
SELECT 6,'BlahG',4 UNION
SELECT 5,'BlahH',3 --UNION
SELECT * FROM #Locations
SELECT * FROM #LocationCache
SELECT top 3 a.LocationId, b.SearchQuery, b.SearchRank
FROM #Locations a
INNER JOIN #LocationCache b ON a.LocationId = b.LocationId
WHERE a.CountryId = 2
AND a.[Type] = 7
DROP TABLE #Locations
DROP TABLE #LocationCache
私の場合、クエリプランは、ネストされたループの内部結合を使用してシークすることを示しています。これを実行すると、両方のシークが発生しますか?その場合は、システムでテストを実行し、LocationsとLocationCacheテーブルのコピーを作成し、すべてのインデックスを使用してLocations2とLocationCache2と呼び、データをそれらにコピーします。次に、新しいテーブルにヒットするクエリを試してみますか?