web-dev-qa-db-ja.com

EXISTSクエリがインデックスシークではなくインデックススキャンを実行するのはなぜですか?

一部のクエリの最適化に取り組んでいます。

以下のクエリの場合、

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はインデックススキャンを実行しています。

  • 統計が更新されました。
  • アイテムテーブルには、41,208行のテストがあります。

アイテムテーブルから列を選択していないことに注意してください。

このアイテムテーブルには、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)

enter image description hereenter image description here

これはテーブルの定義とインデックスです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を使用するようにストアドプロシージャを更新して、関連する列のデータベースのデータ型を一致させました。

その後、クエリプランを見ると、次の図が表示されます。

enter image description here

enter image description here

それがまさに私が望んでいた方法でした。

クエリオプティマイザーの場合は、両方の環境で最適なクエリプランを取得するために適切に機能したと思います。クエリを追加しなかったことをうれしく思います。ヒント。

投稿された3つの答えで学びました。 Max VernonPaul White および Daniel Hutmacher の回答に感謝します。

15

クエリオプティマイザーから良い結果が必要な場合は、データタイプに注意する必要があります

変数はdatetime2として入力されます。

DECLARE @OrderStartDate datetime2 = '27 feb 2016';
DECLARE @OrderEndDate  datetime2 = '28 feb 2016';

しかし、これらが比較される列は次のように入力されますsmalldatetimesdtmプレフィックスが示唆するように!):

[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" を参照してください。

更新:データ型を修正すると、目的のシークプランが得られます。以前に型変換によって引き起こされたカーディナリティ推定エラーは、より遅い計画を与えました。

16
Paul White 9

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';

アーロンの投稿 はそれを明確にするのに役立つかもしれません。

11
Max Vernon

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テーブルの非常に最適なインデックスを使用して、日付列の非クラスター化インデックスから(クラスター化キーに含まれる)注文番号の範囲を識別します。

MIN/MAX query plan

次に、2つのテーブルをそれぞれのクラスター化インデックスでマージ結合できます。

Merge Join of orders and order items

明らかに、テストするデータはありませんが、これは非常に優れたソリューションになるはずです。

6