一部のクエリの最適化に取り組んでいます。
以下のクエリの場合、
SET STATISTICS IO ON;
DECLARE @OrderStartDate DATETIME2 = '27 feb 2016';
DECLARE @OrderEndDate DATETIME2 = '28 feb 2016';
SELECT o.strBxOrderNo
, o.sintOrderStatusID
, o.sintOrderChannelID
, o.sintOrderTypeID
, o.sdtmOrdCreated
, o.sintMarketID
, o.strOrderKey
, o.strOfferCode
, o.strCurrencyCode
, o.decBCShipFullPrice
, o.decBCShipFinal
, o.decBCShipTax
, o.decBCTotalAmount
, o.decWrittenTotalAmount
, o.decBCWrittenTotalAmount
, o.decBCShipOfferDisc
, o.decBCShipOverride
, o.decTotalAmount
, o.decShipTax
, o.decShipFinal
, o.decShipOverride
, o.decShipOfferDisc
, o.decShipFullPrice
, o.lngAccountParticipantID
, CONVERT(DATE, o.sdtmOrdCreated, 120) as OrderCreatedDateConverted
FROM tablebackups.dbo.tblBOrder o
WHERE o.sdtmOrdCreated >= @OrderStartDate
AND o.sdtmOrdCreated < @OrderEndDate
AND EXISTS (
SELECT *
FROM tablebackups.dbo.tblBOrderItem oi
WHERE oi.strBxOrderNo = o.strBxOrderNo
AND oi.decCatItemPrice > 0
)
OPTION (RECOMPILE);
次のFILTEREDインデックスを作成しました。
-- table dbo.tblBorderItem
CREATE NONCLUSTERED INDEX IX_tblBOrderItem_decCatItemPrice_INCL
ON dbo.tblBorderItem
(
strBxOrderNo ASC
, sintOrderSeqNo ASC
, decCatItemPrice
)
INCLUDE
(
blnChargeShipping
, decBCCatItemPrice
, decBCCostPrice
, decBCFinalPrice
, decBCOfferDiscount
, decBCOverrideDiscount
, decBCTaxAmount
, decCostPrice
, decFinalPrice
, decOfferDiscount
, decOverrideDiscount
, decTaxAmount
, decWasPrice
, dtmOrdItemCreated
, sintOrderItemStatusId
, sintOrderItemType
, sintQuantity
, strItemNo
)
WHERE decCatItemPrice > 0
WITH (DROP_EXISTING = ON, FILLFACTOR = 95);
このインデックスは、特にこのクエリだけに使用されるのではなく、この同じインデックスを使用するクエリが他にもあるため、INCLUDED列があります。
特にこのクエリの場合、注文にdecCatItemPrice > 0
。
以下の図に示すように、SQL Serverはインデックススキャンを実行しています。
アイテムテーブルから列を選択していないことに注意してください。
このアイテムテーブルには、164,309,397のライブがあります。そこでのスキャンは避けたいです。
質問:
SQL Serverがインデックスシークを行わないのはなぜですか?
このクエリを改善するために考慮すべき他の要因/事柄はありますか?
(4537 row(s) affected) Table 'tblBorder'. Scan count 1, logical reads
116, physical reads 0, read-ahead reads 0, lob logical reads 0, lob
physical reads 0, lob read-ahead reads 0. Table 'tblBorderItem'. Scan
count 1, logical reads 689, physical reads 0, read-ahead reads 0, lob
logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row(s) affected)
これはテーブルの定義とインデックスですtblBorderItem
IF OBJECT_ID('[dbo].[tblBorderItem]') IS NOT NULL
DROP TABLE [dbo].[tblBorderItem]
GO
CREATE TABLE [dbo].[tblBorderItem] (
[strBxOrderNo] VARCHAR(20) NOT NULL,
[sintOrderSeqNo] SMALLINT NOT NULL,
[sintOrderItemStatusId] SMALLINT NOT NULL,
[sintNameStructureID] SMALLINT NOT NULL,
[strItemNo] VARCHAR(20) NOT NULL,
[sintQuantity] SMALLINT NOT NULL,
[strCurrencyCode] VARCHAR(3) NOT NULL,
[decCostPrice] DECIMAL(18,4) NOT NULL,
[decCatItemPrice] DECIMAL(18,2) NOT NULL,
[decOfferDiscount] DECIMAL(18,2) NOT NULL,
[decOverrideDiscount] DECIMAL(18,2) NOT NULL,
[decFinalPrice] DECIMAL(18,2) NOT NULL,
[decTaxAmount] DECIMAL(18,2) NOT NULL,
[strBCCurrencyCode] VARCHAR(3) NOT NULL,
[decBCCostPrice] DECIMAL(18,4) NOT NULL,
[decBCCatItemPrice] DECIMAL(18,4) NOT NULL,
[decBCOfferDiscount] DECIMAL(18,4) NOT NULL,
[decBCOverrideDiscount] DECIMAL(18,4) NOT NULL,
[decBCFinalPrice] DECIMAL(18,4) NOT NULL,
[decBCTaxAmount] DECIMAL(18,4) NOT NULL,
[dtmOrdItemCreated] DATETIME NOT NULL,
[blnChargeShipping] BIT NOT NULL,
[lngTimeOfOrderQtyOnHand] INT NULL,
[sdtmTimeOfOrderDueDate] SMALLDATETIME NULL,
[lngProdSetSeqNo] INT NULL,
[lngProdRelationId] INT NULL,
[lngProdRelationMemberId] INT NULL,
[decWasPrice] DECIMAL(18,2) NULL,
[sintOrderItemType] SMALLINT NULL,
[tsRowVersion] TIMESTAMP NULL,
[sdtmOrderItemStatusUpdated] SMALLDATETIME NULL,
CONSTRAINT [PK_tblBOrderItem]
PRIMARY KEY CLUSTERED
([strBxOrderNo] asc, [sintOrderSeqNo] asc)
WITH FILLFACTOR = 100)
GO
CREATE NONCLUSTERED INDEX
[IX_tblBOrderItem__dtmOrdItemCreated]
ON [dbo].[tblBorderItem] ([dtmOrdItemCreated] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX [IX_tblBOrderItem__sintOrderItemStatusId]
ON [dbo].[tblBorderItem] ([sintOrderItemStatusId] asc)
INCLUDE ([sdtmOrderItemStatusUpdated],
[sintOrderSeqNo], [strBxOrderNo], [strItemNo])
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX [IX_tblBOrderItem__
sintOrderItemStatusId_decFinalPrice_
sdtmOrderItemStatusUpdated_
include_strBxOrderNo]
ON [dbo].[tblBorderItem]
([sintOrderItemStatusId] asc,
[decFinalPrice] asc,
[sdtmOrderItemStatusUpdated] asc)
INCLUDE ([strBxOrderNo])
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX [IX_tblBOrderItem__strBxOrderNo]
ON [dbo].[tblBorderItem]
([strBxOrderNo] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX [IX_tblBOrderItem__strItemNo]
ON [dbo].[tblBorderItem] ([strItemNo] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrderItem_decCatItemPrice_INCL]
ON [dbo].[tblBorderItem]
([strBxOrderNo] asc, [sintOrderSeqNo] asc, [decCatItemPrice] asc)
INCLUDE ([blnChargeShipping],
[decBCCatItemPrice], [decBCCostPrice], [decBCFinalPrice],
[decBCOfferDiscount], [decBCOverrideDiscount],
[decBCTaxAmount], [decCostPrice], [decFinalPrice],
[decOfferDiscount], [decOverrideDiscount],
[decTaxAmount], [decWasPrice], [dtmOrdItemCreated],
[sintOrderItemStatusId], [sintOrderItemType],
[sintQuantity], [strItemNo])
WHERE ([decCatItemPrice]>(0))
WITH FILLFACTOR = 95
これはテーブルの定義とインデックスですtblBorder
IF OBJECT_ID('[dbo].[tblBorder]') IS NOT NULL
DROP TABLE [dbo].[tblBorder]
GO
CREATE TABLE [dbo].[tblBorder] (
[strBxOrderNo] VARCHAR(20) NOT NULL,
[uidOrderUniqueID] UNIQUEIDENTIFIER NOT NULL,
[sintOrderStatusID] SMALLINT NOT NULL,
[sintOrderChannelID] SMALLINT NOT NULL,
[sintOrderTypeID] SMALLINT NOT NULL,
[blnIsBasket] BIT NOT NULL,
[sdtmOrdCreated] SMALLDATETIME NOT NULL,
[sintMarketID] SMALLINT NOT NULL,
[strOrderKey] VARCHAR(20) NOT NULL,
[strOfferCode] VARCHAR(20) NOT NULL,
[lngShippedToParticipantID] INT NOT NULL,
[lngOrderedByParticipantID] INT NOT NULL,
[lngShipToAddressID] INT NOT NULL,
[lngAccountAddressID] INT NOT NULL,
[lngAccountParticipantID] INT NOT NULL,
[lngOrderedByAddressID] INT NOT NULL,
[lngOrderTakenBy] INT NOT NULL,
[strCurrencyCode] VARCHAR(3) NOT NULL,
[decShipFullPrice] DECIMAL(18,2) NOT NULL,
[decShipOfferDisc] DECIMAL(18,2) NOT NULL,
[decShipOverride] DECIMAL(18,2) NOT NULL,
[decShipFinal] DECIMAL(18,2) NOT NULL,
[decShipTax] DECIMAL(18,2) NOT NULL,
[strBCCurrencyCode] VARCHAR(3) NOT NULL,
[decBCShipFullPrice] DECIMAL(18,4) NOT NULL,
[decBCShipOfferDisc] DECIMAL(18,4) NOT NULL,
[decBCShipOverride] DECIMAL(18,4) NOT NULL,
[decBCShipFinal] DECIMAL(18,4) NOT NULL,
[decBCShipTax] DECIMAL(18,4) NOT NULL,
[decTotalAmount] DECIMAL(18,2) NOT NULL,
[decBCTotalAmount] DECIMAL(18,4) NOT NULL,
[decWrittenTotalAmount] DECIMAL(18,2) NULL,
[decBCWrittenTotalAmount] DECIMAL(18,4) NULL,
[blnProRataShipping] BIT NOT NULL,
[blnChargeWithFirstShipment] BIT NOT NULL,
[sintShippingServiceLevelID] SMALLINT NOT NULL,
[sintShippingMethodID] SMALLINT NOT NULL,
[sdtmDoNotShipUntil] SMALLDATETIME NULL,
[blnHoldUntilComplete] BIT NOT NULL,
[tsRowVersion] TIMESTAMP NULL,
CONSTRAINT [PK_tblBOrder]
PRIMARY KEY CLUSTERED
([strBxOrderNo] asc) WITH FILLFACTOR = 100)
GO
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__lngAccountAddressID]
ON [dbo].[tblBorder]
([lngAccountAddressID] asc, [sintOrderStatusID] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__lngAccountParticipantID]
ON [dbo].[tblBorder]
([lngAccountParticipantID] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__lngOrderedByAddressID]
ON [dbo].[tblBorder]
([lngOrderedByAddressID] asc, [sintOrderStatusID] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__lngOrderedByParticipantID]
ON [dbo].[tblBorder] ([lngOrderedByParticipantID] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__lngShippedToParticipantID]
ON [dbo].[tblBorder]
([lngShippedToParticipantID] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__lngShipToAddressID]
ON [dbo].[tblBorder]
([lngShipToAddressID] asc, [sintOrderStatusID] asc)
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder__sdtmOrdCreated_sintMarketID__include_strBxOrderNo]
ON [dbo].[tblBorder]
([sdtmOrdCreated] asc, [sintMarketID] asc)
INCLUDE ([strBxOrderNo])
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX
[IX_tblBOrder_sdtmOrdCreated_INCL]
ON [dbo].[tblBorder]
([sdtmOrdCreated] asc)
INCLUDE ([decBCShipFinal], [decBCShipFullPrice],
[decBCShipOfferDisc], [decBCShipOverride],
[decBCShipTax], [decBCTotalAmount], [decBCWrittenTotalAmount],
[decShipFinal], [decShipFullPrice], [decShipOfferDisc],
[decShipOverride], [decShipTax], [decTotalAmount],
[decWrittenTotalAmount], [lngAccountParticipantID],
[lngOrderedByParticipantID], [sintMarketID],
[sintOrderChannelID], [sintOrderStatusID],
[sintOrderTypeID], [strBxOrderNo], [strCurrencyCode],
[strOfferCode], [strOrderKey])
WITH FILLFACTOR = 100
CREATE NONCLUSTERED
INDEX [IX_tblBOrder_sintMarketID_sdtmOrdCreated]
ON [dbo].[tblBorder]
([sintMarketID] asc, [sdtmOrdCreated] asc)
INCLUDE ([sintOrderChannelID], [strBxOrderNo])
WITH FILLFACTOR = 100
CREATE NONCLUSTERED
INDEX [IX_tblBOrder__sintOrderChannelID_sdtmOrdCreated_INCL]
ON [dbo].[tblBorder]
([sintOrderChannelID] asc, [sdtmOrdCreated] asc)
INCLUDE ([decBCShipFinal], [decBCShipFullPrice],
[decBCShipTax], [decShipFinal], [decShipFullPrice],
[decShipTax], [lngAccountParticipantID], [sintMarketID],
[sintOrderTypeID], [strBxOrderNo],
[strCurrencyCode], [strOrderKey])
WITH FILLFACTOR = 100
CREATE NONCLUSTERED INDEX [IX_tblBOrder_strBxOrderNo_sdtmOrdCreated_incl]
ON [dbo].[tblBorder] ([strBxOrderNo] asc,
[sdtmOrdCreated] asc)
INCLUDE ([sintOrderChannelID], [sintOrderTypeID], [sintMarketID],
[strOrderKey], [lngAccountParticipantID], [strCurrencyCode],
[decShipFullPrice], [decShipFinal], [decShipTax],
[decBCShipFullPrice], [decBCShipFinal],
[decBCShipTax])
結論
LIVEシステムにインデックスを適用し、SMALLDATETIMEを使用するようにストアドプロシージャを更新して、関連する列のデータベースのデータ型を一致させました。
その後、クエリプランを見ると、次の図が表示されます。
それがまさに私が望んでいた方法でした。
クエリオプティマイザーの場合は、両方の環境で最適なクエリプランを取得するために適切に機能したと思います。クエリを追加しなかったことをうれしく思います。ヒント。
投稿された3つの答えで学びました。 Max Vernon 、 Paul White および Daniel Hutmacher の回答に感謝します。
クエリオプティマイザーから良い結果が必要な場合は、データタイプに注意する必要があります。
変数はdatetime2として入力されます。
DECLARE @OrderStartDate datetime2 = '27 feb 2016';
DECLARE @OrderEndDate datetime2 = '28 feb 2016';
しかし、これらが比較される列は次のように入力されますsmalldatetime(sdtmプレフィックスが示唆するように!):
[sdtmOrdCreated] SMALLDATETIME NOT NULL
型の非互換性は、オプティマイザが型変換を通じて結果として得られるカーディナリティの見積もりを計算することを困難にします 、実行プランのxmlに示すように:
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@OrderStartDate],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes(NULL,[@OrderEndDate],(10))">
現在の見積もりは正確である場合とそうでない場合があります(おそらく正確ではありません)。タイプの非互換性を修正すると、プランの選択の問題が完全に解決される場合と解決されない場合がありますが、問題を詳しく調べる前に、これが最初に(簡単に)解決できる問題です。
DECLARE @OrderStartDate smalldatetime = CONVERT(smalldatetime, '20160227', 112);
DECLARE @OrderEndDate smalldatetime = CONVERT(smalldatetime, '20160228', 112);
常にカーディナリティの見積もりの精度を確認し、クエリの書き換えを決定する前に、不一致の理由を確認してくださいまたはヒントを使用します。
動的シークの詳細については、SQLblog.comの記事 "Dynamic Seeks and Hidden Implicit Conversions" を参照してください。
更新:データ型を修正すると、目的のシークプランが得られます。以前に型変換によって引き起こされたカーディナリティ推定エラーは、より遅い計画を与えました。
SQL Serverは、必要な各行を探すよりもコストが安いと考えているため、インデックススキャンを実行しています。ほとんどの場合、SQL Serverは正しいですが、セットアップでの選択肢があります。
インデックス全体をスキャンするのではなく、SQL Serverが実際にインデックスに対して範囲スキャンを実行している場合があることに注意してください。
両方のテーブルのDDLを、他のインデックスと一緒に提供すると、リソースを大幅に節約できるようになる可能性があります。
補足として、そのような日付リテラルは決して使用しないでください。の代わりに:
DECLARE @OrderStartDate DATETIME2 = '27 feb 2016';
DECLARE @OrderEndDate DATETIME2 = '28 feb 2016';
これを使って:
DECLARE @OrderStartDate DATETIME2 = '2016-02-27T00:00:00.0000';
DECLARE @OrderEndDate DATETIME2 = '2016-02-28T00:00:00.0000';
アーロンの投稿 はそれを明確にするのに役立つかもしれません。
Max's answer に追加するには、クエリを2つの部分に分割してみます。
DECLARE @OrderStartDate DATETIME2 = {d '2016-02-27'};
DECLARE @OrderEndDate DATETIME2 = {d '2016-02-28'};
--- Work variable declarations:
DECLARE @minOrderNo varchar(20), @maxOrderNo varchar(20);
--- Find the lowest and highest order number respectively for
--- your date range:
SELECT @minOrderNo=MIN(strBxOrderNo),
@maxOrderNo=MAX(strBxOrderNo)
FROM dbo.tblBOrder o
WHERE o.sdtmOrdCreated >= @OrderStartDate AND
o.sdtmOrdCreated < @OrderEndDate;
--- Join orders and order items on their respective clustering keys.
SELECT o.strBxOrderNo
, o.sintOrderStatusID
, o.sintOrderChannelID
, o.sintOrderTypeID
, o.sdtmOrdCreated
, o.sintMarketID
, o.strOrderKey
, o.strOfferCode
, o.strCurrencyCode
, o.decBCShipFullPrice
, o.decBCShipFinal
, o.decBCShipTax
, o.decBCTotalAmount
, o.decWrittenTotalAmount
, o.decBCWrittenTotalAmount
, o.decBCShipOfferDisc
, o.decBCShipOverride
, o.decTotalAmount
, o.decShipTax
, o.decShipFinal
, o.decShipOverride
, o.decShipOfferDisc
, o.decShipFullPrice
, o.lngAccountParticipantID
, CONVERT(DATE, o.sdtmOrdCreated, 120) as OrderCreatedDateConverted
FROM dbo.tblBOrder AS o
INNER /*MERGE*/ JOIN dbo.tblBOrderItem AS oi ON
o.strBxOrderNo>=@minOrderNo AND --- OrderNo filter on "orders"
o.strBxOrderNo<=@maxOrderNo AND
oi.strBxOrderNo=o.strBxOrderNo AND --- Equijoin
oi.strBxOrderNo>=@minOrderNo AND --- OrderNo filter on "order items"
oi.strBxOrderNo<=@maxOrderNo AND
oi.decCatItemPrice > 0 --- Item price filter on "order items"
OPTION (RECOMPILE);
このクエリは、(a)ソート演算子を排除します(これは、ブロッキング演算子であり、メモリの付与が必要であるためコストがかかります)、(b)マージ結合を作成します(結合ヒントで強制できますが、十分なデータがあると自動的に行われます) )。おまけとして、(c)インデックススキャンも削除されます。
全体として、MIN/MAXクエリは、Ordersテーブルの非常に最適なインデックスを使用して、日付列の非クラスター化インデックスから(クラスター化キーに含まれる)注文番号の範囲を識別します。
次に、2つのテーブルをそれぞれのクラスター化インデックスでマージ結合できます。
明らかに、テストするデータはありませんが、これは非常に優れたソリューションになるはずです。