web-dev-qa-db-ja.com

(SELECT)クエリで使用されていないインデックスを作成すると、そのクエリのパフォーマンスが低下します

私はピナル・デイブの このビデオ を見たところです。

彼には、tempdbで〜370kの読み取りを生成するSELECTクエリと、クエリのSELECTingであるテーブルの〜1200の読み取りがあります。

次に、tempdbスプールを削除して、クエリのパフォーマンスを向上させるインデックス(これをIndex1と呼びます)を作成します。これまでのところすべてOK。

ただし、その後、追加のインデックス(Index2と呼びます)を作成し、Index1をそのまま残します。

その後、クエリを再度実行すると、Index2が使用されていなくても、クエリのパフォーマンスは元の状態に戻り、約370kのtempdbスプールがまだ残っています。

彼は実際にこれを引き起こす原因を説明していないようです(私が見逃した場合を除きます)

再現するコードは以下のとおりです(Pastebinを提供してくれたMartin Smithに感謝)これは、SalesOrderDetailに標準インデックスを持つAdventureWorksのバニラバージョンを想定しています。

SET STATISTICS XML ON;
SET STATISTICS IO ON
GO

-- The query
DBCC FREEPROCCACHE;
SELECT SalesOrderID, ProductId,SalesOrderDetailID, OrderQty
FROM   Sales.SalesOrderDetail sod
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail sod1
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID);
/* 
(11110 rows affected)
Table 'Worktable'. Scan count 3, logical reads 368495, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 1246, physical reads 2, page server reads 0, read-ahead reads 1284, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
*/

ここ は、何もする前の計画です(ネストされたループとテーブルスプール)。

次に、インデックスを作成します

CREATE NONCLUSTERED INDEX IX_Index1 ON Sales.SalesOrderDetail (SalesOrderID, ProductId) INCLUDE (SalesOrderDetailID, OrderQty);

その後、クエリを再度実行して、改善された計画とtempdbスプールがなくなったことを確認できます。

DBCC FREEPROCCACHE;
SELECT SalesOrderID, ProductId,SalesOrderDetailID, OrderQty
FROM   Sales.SalesOrderDetail sod
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail sod1
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID);
/*
(11110 rows affected)
Table 'SalesOrderDetail'. Scan count 2, logical reads 608, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
*/

計画

次に、別の(クエリには役に立たない)インデックスを作成します

CREATE NONCLUSTERED INDEX IX_Index2 ON Sales.SalesOrderDetail (ProductId,SalesOrderID) INCLUDE (SalesOrderDetailID, OrderQty);

次に、クエリを再度実行します。

-- Run the same query again
DBCC FREEPROCCACHE;
SELECT SalesOrderID, ProductId,SalesOrderDetailID, OrderQty
FROM   Sales.SalesOrderDetail sod
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail sod1
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID);

/*
(11110 rows affected)
Table 'Worktable'. Scan count 3, logical reads 368495, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 304, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
*/

読み取りと 計画 は、インデックスを追加する前と同じです。

IX_Index1を強制的に使用することもできます。

-- Run the query an force the index
DBCC FREEPROCCACHE;
SELECT SalesOrderID, ProductId,SalesOrderDetailID, OrderQty
FROM   Sales.SalesOrderDetail sod WITH (INDEX = IX_Index1)
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail sod1 WITH (INDEX = IX_Index1)
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID);
/*
(11110 rows affected)
Table 'Worktable'. Scan count 3, logical reads 368495, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table '

そして、私はまだ spools plan を取得します

再び以下を守ったマーティン・スミスに感謝します。

IX_Index2を作成してクエリを実行すると、不良プランが使用されます(クエリには役に立たないため、理にかなっています)。ただし、IX_Index1を作成してクエリを実行すると、適切なプランはこのように使用すると、問題であるのは両方のインデックスが存在するのではなく、作成された順序であることが示唆されます。

私も このビデオ を見て、Pinalが同じ問題について提示しています。彼はインデックスがSELECTのパフォーマンスを低下させることを示唆しているようで、この問題のために完全に削除する必要があります(私が誤解している場合を除きます)。

私の最初の質問は次のとおりです。何がこの動作を引き起こしますか?

私の2番目の質問は、この動作はどれほど一般的であるかです(2番目のビデオでは、インデックスをまったく作成しないことを提案した場合、かなり一般的だと思います)

10
SEarle1986

ヒントを追加することにより、より良い実行計画構造を取得しようとしています

ImplRestrRemap

クエリルールImplRestrRemapは、インデックスが追加されたときにセカンダリクエリで使用されます。インデックスが追加されると、マージジョインよりもクエリバックでコストが低くなると推定されます。これは問題の結果です。特定のケースでマージ結合を使用するよりもサブツリーのコストが低い理由は、さらに下にあります。

トレースフラグを追加して出力ツリーをチェックすると、このルールの使用法を見つけることができます。

_OPTION(
RECOMPILE, 
QUERYTRACEON 3604,
QUERYTRACEON 8607)
_

クエリにOPTION(QUERYRULEOFF ImplRestrRemap)を追加して、ルールを無効にすることができます。

_SELECT SalesOrderID, ProductId,SalesOrderDetailID, OrderQty
FROM   Sales.SalesOrderDetail sod
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail sod1
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID)
OPTION 
(
    QUERYRULEOFF ImplRestrRemap
);
_

これにより、マージ結合の代わりにハッシュ結合が提供され、スプールプランよりも推定されるサブツリーのコストが高くなります。もう1つの違いは、1つに2回アクセスする代わりに、作成された2つの非クラスター化インデックスを使用することです。

enter image description here

マージ結合

マージ結合をプランに追加してルールを無効にしようとすると、クエリプランに並べ替えが追加されます。

_OPTION 
(
    QUERYRULEOFF ImplRestrRemap, 
    MERGE JOIN
);
_

enter image description here

推定されるサブツリーコストはハッシュ結合よりも高く、このハッシュ結合の推定されるサブツリーコストは、スプールプランよりも高くなります。ヒントなしで計画の選択を説明する。

ここでも、作成された2つのNCインデックスを使用しています。

インデックスのヒント

両方のテーブルアクセスで同じインデックスを使用して「サンプル」プランを取得しようとする場合は、インデックスヒントを追加できます。

_SELECT SalesOrderID, ProductId,SalesOrderDetailID, OrderQty
FROM   Sales.SalesOrderDetail sod WITH(INDEX(IX_SalesOrderID_ProductId))
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail  sod1 WITH(INDEX(IX_SalesOrderID_ProductId))
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID)
OPTION 
(
    QUERYRULEOFF ImplRestrRemap, 
    MERGE JOIN
);
_

これにより、別のソート演算子が追加されます。

enter image description here

enter image description here

違い

結合方向

マージ結合の内側と外側の結合列が入れ替えられるため

ソートマージ結合実行プランなし

_<InnerSideJoinColumns>
  <ColumnReference Database="[AdventureWorks2017]" Schema="[Sales]" Table="[SalesOrderDetail]" Alias="[sod]" Column="SalesOrderID" />
  <ColumnReference Database="[AdventureWorks2017]" Schema="[Sales]" Table="[SalesOrderDetail]" Alias="[sod]" Column="ProductID" />
</InnerSideJoinColumns>
<OuterSideJoinColumns>
  <ColumnReference Database="[AdventureWorks2017]" Schema="[Sales]" Table="[SalesOrderDetail]" Alias="[sod1]" Column="SalesOrderID" />
  <ColumnReference Column="Expr1002" />
</OuterSideJoinColumns>
_

ダブルソートマージ結合実行プラン

_<InnerSideJoinColumns>
  <ColumnReference Database="[AdventureWorks2017]" Schema="[Sales]" Table="[SalesOrderDetail]" Alias="[sod]" Column="ProductID" />
  <ColumnReference Database="[AdventureWorks2017]" Schema="[Sales]" Table="[SalesOrderDetail]" Alias="[sod]" Column="SalesOrderID" />
</InnerSideJoinColumns>
<OuterSideJoinColumns>
  <ColumnReference Column="Expr1002" />
  <ColumnReference Database="[AdventureWorks2017]" Schema="[Sales]" Table="[SalesOrderDetail]" Alias="[sod1]" Column="SalesOrderID" />
</OuterSideJoinColumns>
_

パフォーマンスの高いクエリプランは、次のインデックスキーパスを通過できるという意味で

--> SalesOrderId - SalesOrderId --> ProductId - ProductId (Agg)

並べ替えクエリプランは従うことができますが

--> ProductId (Agg) - ProductId --> SalesOrderId - SalesOrderId

また、追加されたソートが原因で、ダブルソートマージ結合プランを使用すると、インデックススキャンは順序付けられません。

_ <IndexScan Ordered="false" ForcedIndex="true" ForceSeek="false" ForceScan="false" NoExpandHint="false" Storage="RowStore">
_

パフォーマンスの良いクエリプランを強制しようとしています

正しいプランを強制しようとするとき、たとえば、問題を提供しない順序でインデックスを作成してからクエリを実行し、プランxmlをキャプチャする場合:

_drop index if exists IX_ProductId_SalesOrderID on Sales.SalesOrderDetail;
drop index if exists IX_SalesOrderID_ProductId on Sales.SalesOrderDetail;

CREATE NONCLUSTERED INDEX IX_ProductId_SalesOrderID ON Sales.SalesOrderDetail (ProductId,SalesOrderID) INCLUDE (SalesOrderDetailID, OrderQty);
CREATE NONCLUSTERED INDEX IX_SalesOrderID_ProductId ON Sales.SalesOrderDetail (SalesOrderID, ProductId) INCLUDE (SalesOrderDetailID, OrderQty);
_

次に、そのプランXMLを使用して、インデックスを別の順序で再作成します。

_drop index if exists IX_ProductId_SalesOrderID on Sales.SalesOrderDetail;
drop index if exists IX_SalesOrderID_ProductId on Sales.SalesOrderDetail;
CREATE NONCLUSTERED INDEX IX_SalesOrderID_ProductId ON Sales.SalesOrderDetail (SalesOrderID, ProductId) INCLUDE (SalesOrderDetailID, OrderQty);
CREATE NONCLUSTERED INDEX IX_ProductId_SalesOrderID ON Sales.SalesOrderDetail (ProductId,SalesOrderID) INCLUDE (SalesOrderDetailID, OrderQty);
_

_USE PLAN_ HINTは、同じクエリと同じインデックスのエラーを報告します。

メッセージ8698、レベル16、状態0、行1クエリプロセッサはクエリプランを作成できませんでした。USEPLANヒントにクエリに対して正当であると確認できなかったプランが含まれているためです。 USE PLANヒントを削除または置き換えます。プランの強制が成功する可能性を最大限に高めるには、USE PLANヒントで提供されるプランが、同じクエリに対してSQL Serverによって自動的に生成されるものであることを確認してください。

マージ結合で同じ並べ替え順序を使用することは不可能であり、マージ結合の最適なオプションとして並べ替え演算子を使用するには元に戻す必要があります。

これとは逆に、パフォーマンスの良いマージ結合プランを使用できる場合に、_USE PLAN_ヒ​​ントを使用してスプールプランをクエリに適用することは実際に可能です。

これで取り上げられたポイント Q/A これがなぜ起こっているのかについて、より多くの情報を提供します。

解決

並べ替え

前述の Q/A で指定されているとおりに順序を追加すると、インデックスの作成順序が異なる場合でも、正しい実行プランが選択されます。

_SELECT SalesOrderID, ProductId
FROM   Sales.SalesOrderDetail2 sod 
WHERE  ProductID = (SELECT AVG(ProductID)
                    FROM   Sales.SalesOrderDetail2  sod1  
                    WHERE  sod.SalesOrderID = sod1.SalesOrderID
                    GROUP  BY sod1.SalesOrderID
                    )
ORDER BY SalesOrderID;
_

enter image description here

4
Randi Vertongen