Pourquoi ma requête EXISTS effectue-t-elle une analyse d'index au lieu d'une recherche d'index?

15

Je travaille sur l'optimisation de certaines requêtes.

Pour la requête ci-dessous,

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

J'ai créé l'index FILTRÉ suivant:

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

Cet index n'est pas utilisé uniquement pour cette requête en particulier, il existe d'autres requêtes qui utilisent ce même index, donc les colonnes INCLUDED.

Pour cette requête en particulier, je veux juste vérifier (EXISTE) si une commande a un article où decCatItemPrice > 0.

SQL Server effectue une analyse d'index comme vous pouvez le voir dans les images ci-dessous.

  • Les statistiques viennent d'être mises à jour.
  • Le tableau des éléments comporte 41 208 lignes en test.

Veuillez noter que je ne sélectionne aucune colonne dans le tableau des éléments.

Ce tableau d'articles a 164 309 397 en direct. Je voudrais éviter une analyse là-bas.

des questions:

Pourquoi SQL Server ne fait-il pas de recherche d'index?

Y a-t-il d'autres facteurs / éléments à prendre en compte pour améliorer cette requête?

(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)

entrez la description de l'image ici entrez la description de l'image ici

c'est la définition et les index de la table 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

c'est la définition et les index de la table 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])

Conclusion

J'ai appliqué mon index sur le système LIVE et mis à jour ma procédure stockée pour utiliser SMALLDATETIME, afin de faire correspondre les types de données dans la base de données pour les colonnes impliquées.

Après cela, lorsque je regarde le plan de requête, je vois l'image ci-dessous:

entrez la description de l'image ici

entrez la description de l'image ici

c'était exactement comme je le voulais.

Je pense que l'optimiseur de requêtes dans ce cas a fait un bon travail pour obtenir le meilleur plan de requête sur les deux environnements, et je suis heureux de ne pas avoir ajouté d'indices de requête.

J'ai appris avec les 3 réponses affichées. merci à Max Vernon , Paul White et Daniel Hutmacher pour leurs réponses.

Marcello Miorelli
la source

Réponses:

15

Si vous souhaitez obtenir de bons résultats de l'optimiseur de requêtes, il vaut la peine de faire attention aux types de données .

Vos variables sont saisies comme datetime2 :

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

Mais la colonne à laquelle ils sont comparés est tapée smalldatetime (comme le préfixe sdtm le suggère!):

[sdtmOrdCreated] SMALLDATETIME NOT NULL

L'incompatibilité de type rend difficile pour l'optimiseur de calculer l'estimation de cardinalité résultante via une conversion de type , comme indiqué dans le plan d'exécution xml:

<ScalarOperator ScalarString="GetRangeWithMismatchedTypes([@OrderStartDate],NULL,(22))">
<ScalarOperator ScalarString="GetRangeWithMismatchedTypes(NULL,[@OrderEndDate],(10))">

L'estimation actuelle peut être exacte ou non (probablement pas). Résoudre l'incompatibilité de type peut ou non résoudre complètement votre problème de sélection de plan, mais c'est la première chose (facile!) Que je corrigerais avant d'approfondir le problème:

DECLARE @OrderStartDate smalldatetime = CONVERT(smalldatetime, '20160227', 112);
DECLARE @OrderEndDate smalldatetime = CONVERT(smalldatetime, '20160228', 112);

Vérifiez toujours l'exactitude des estimations de cardinalité et la raison de tout écart avant de décider de réécrire la requête ou d'utiliser des conseils.

Voir mon article SQLblog.com, " Recherches dynamiques et conversions implicites cachées" pour plus de détails sur la recherche dynamique.

Mise à jour: la correction du type de données vous a permis d'obtenir le plan de recherche que vous vouliez. Les erreurs d'estimation de cardinalité causées par la conversion de type avant vous ont donné le plan le plus lent.

Paul White 9
la source
11

SQL Server effectue une analyse d'index car il pense que c'est moins cher que de rechercher chaque ligne requise. Très probablement, SQL Server est correct, compte tenu des choix qu'il a dans votre configuration.

Sachez que SQL Server peut en fait effectuer une analyse de plage sur l'index, par opposition à l'analyse de l'index entier.

Si vous fournissez le DDL pour les deux tables, ainsi que les autres index que vous pouvez avoir, nous pouvons être en mesure de vous aider à rendre cela beaucoup moins gourmand en ressources.

En remarque, n'utilisez jamais de littéraux de date comme ça. Au lieu de:

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

utilisez ceci:

DECLARE @OrderStartDate DATETIME2 = '2016-02-27T00:00:00.0000';
DECLARE @OrderEndDate  DATETIME2 = '2016-02-28T00:00:00.0000';

Le message d'Aaron pourrait aider à clarifier cela.

Max Vernon
la source
6

Pour compléter la réponse de Max, j'essaierais probablement de diviser votre requête en deux parties:

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

Cette requête (a) éliminera l'opérateur de tri (ce qui coûte cher car il bloque et nécessite une allocation de mémoire), (b) créera une jointure de fusion (que vous pourriez forcer avec un indice de jointure, mais cela devrait se produire automatiquement avec suffisamment de données ). En prime, il (c) éliminera également le balayage d'index.

Dans l'ensemble, la requête MIN / MAX utilise un index hautement optimal sur la table Orders pour identifier une plage de numéros de commande (inclus dans la clé de clustering) à partir d'un index non cluster sur la colonne date:

Plan de requête MIN / MAX

Ensuite, vous pouvez fusionner Joindre les deux tables sur leurs index cluster respectifs:

Fusionner la jointure des commandes et des articles de commande

Évidemment, je n'ai pas vos données pour tester, mais j'imagine que cela devrait être une solution très performante.

Daniel Hutmacher
la source
+1 J'ai vraiment aimé la façon de penser ici. Cela n'a pas fonctionné pour ce scénario en particulier, mais j'ai eu l'idée.
Marcello Miorelli