Index filtré utilisé uniquement lorsque la partie filtrée est dans le JOIN et non le WHERE

10

J'ai créé l'index filtré ci-dessous cependant quand j'exécute les 2 requêtes plus bas, cet index n'est utilisé que pour une recherche dans le premier exemple qui a END_DTTM dans JOIN plutôt que la clause where (c'est la seule différence dans les requêtes) . Quelqu'un peut-il expliquer pourquoi cela se produit?

Création d'index

CREATE NONCLUSTERED INDEX [ix_PATIENT_LIST_BESPOKE_LIST_ID_includes] ON [dbo].[PATIENT_LIST_BESPOKE] 
(
    [LIST_ID] ASC,
    [END_DTTM] ASC
)
WHERE ([END_DTTM] IS NULL)

Requêtes

DECLARE @LIST_ID INT = 3655

--This one seeks on the index

SELECT  
    PATIENT_LISTS.LIST_ID
FROM    
    DBO.PATIENT_LISTS
    LEFT JOIN DBO.PATIENT_LIST_BESPOKE ON PATIENT_LISTS.LIST_ID = PATIENT_LIST_BESPOKE.LIST_ID  
                                      AND PATIENT_LIST_BESPOKE.END_DTTM IS NULL
WHERE
    PATIENT_LISTS.LIST_ID = @LIST_ID

--This one scans on the index

SELECT  
    PATIENT_LISTS.LIST_ID
FROM    
    DBO.PATIENT_LISTS
    LEFT JOIN DBO.PATIENT_LIST_BESPOKE ON PATIENT_LISTS.LIST_ID = PATIENT_LIST_BESPOKE.LIST_ID  
WHERE   
    PATIENT_LISTS.LIST_ID = @LIST_ID AND
    PATIENT_LIST_BESPOKE.END_DTTM IS NULL   
chris
la source

Réponses:

12

Pour que l'optimiseur fasse correspondre un prédicat à un index (filtré ou non), le prédicat doit apparaître à côté de l'opération Get dans l'arborescence de requête logique. Pour faciliter cela, les prédicats sont généralement poussés le plus près possible des feuilles de l'arbre logique avant le début de l'optimisation.

Pour simplifier grandement, l'implémentation de la stratégie d'index physique fait ceci:

Predicate + Logical Get -> Physical Get (using Index)

La requête qui vous intéresse commence par le prédicat au-dessus d'une jointure externe:

Predicate on T2 --+-- LOJ -- Get (T1)
                       |
                       +---- Get (T2)

Cette forme ne correspond pas à la règle de stratégie d'index car le prédicat n'est pas adjacent à Get. Ainsi, la première partie de la réponse est que la correspondance d'index filtré échouera à moins que le prédicat ne puisse être poussé au-delà de la jointure externe.

La deuxième partie est simplement que l'optimiseur ne contient pas la règle d'exploration nécessaire pour déplacer un prédicat au-delà d'une jointure externe du côté préservé, car la transformation est si rarement valide. C'est une caractéristique générale de l'optimiseur que seules les règles les plus fréquemment utiles sont implémentées.

Par conséquent, la correspondance de l'index filtré échoue dans ce cas. Pour être clair, la réécriture serait valide dans le cas très spécifique que vous citez (deuxième requête).

Pour le premier formulaire de requête (avec une sémantique différente), le prédicat est associé à la jointure depuis le début, et la logique déroulante du prédicat peut déplacer cette distance sur la courte distance car il n'a pas besoin de dépasser une jointure externe comme expliqué ci-dessus.

Contexte et informations complémentaires:

Paul White 9
la source
9

Ce ne sont pas sémantiquement les mêmes requêtes, car l'une peut filtrer avant la jointure l'autre peut filtrer après. Permettez-moi d'illustrer avec un exemple plus simple:

CREATE TABLE dbo.Lefty(LeftyID INT PRIMARY KEY);

CREATE TABLE dbo.Righty(LeftyID INT, SomeList INT);

INSERT dbo.Lefty(LeftyID) VALUES(1),(2),(3);

INSERT dbo.Righty(LeftyID, SomeList) VALUES(1,1),(1,NULL),(2,2);

La requête 1 renvoie les trois lignes:

SELECT l.LeftyID, r.SomeList
FROM dbo.Lefty AS l
LEFT OUTER JOIN dbo.Righty AS r
ON l.LeftyID = r.LeftyID
AND r.SomeList IS NULL;

La requête 2, cependant, laisse LeftyID 2:

SELECT l.LeftyID, r.SomeList
FROM dbo.Lefty AS l
LEFT OUTER JOIN dbo.Righty AS r
ON l.LeftyID = r.LeftyID
WHERE r.SomeList IS NULL;

Preuve SQLfiddle

Si vous essayez d'effectuer une jointure anti-semi, la colonne testée ne doit pas être nullable . Le déplacement des critères entre ON et WHERE ne fait aucune différence logique lorsque vous traitez uniquement avec des jointures INNER, mais avec OUTER il y a une différence significative. Et vous devez vous soucier davantage de l'exactitude de vos résultats que de la possibilité d'utiliser ou non un index filtré.

Aaron Bertrand
la source
merci pour la réponse mais je ne prétends pas que les requêtes sont les mêmes, je demande pourquoi une requête utilise l'index filtré et l'autre pas.
chris
@chris Avez-vous essayé de forcer cet index avec un indice d'index? Je serais curieux de comparer les plans réels post-exécution avec et sans cet indice. Pour moi, il est clair que l'optimiseur ignore cet index lorsqu'il pense qu'il fait une jointure anti-semi (car il ne s'attendrait pas à ce qu'une colonne nullable soit utilisée dans ce cas), mais je ne suis pas sûr que ce soit le cas. faire avec le coût ou l'ordre des opérations ou une certaine connaissance sous-jacente qu'il y a potentiellement beaucoup plus de lignes venant du côté gauche que celles qui sont dans l'index filtré. Voir les plans pourrait aider.
Aaron Bertrand
3

Les deux requêtes sont différentes - en termes de signification et de résultats. Voici une réécriture, il est donc plus évident de savoir ce que font les deux requêtes:

-- 1st query
SELECT  
    a.LIST_ID
FROM    
      ( SELECT LIST_ID 
        FROM   DBO.PATIENT_LISTS
        WHERE  LIST_ID = @LIST_ID
      ) AS a
    LEFT JOIN  
      ( SELECT LIST_ID                    -- the filtered index
        FROM   DBO.PATIENT_LIST_BESPOKE   -- can be used
        WHERE  END_DTTM IS NULL           -- for the subquery
      ) AS b
    ON  a.LIST_ID = b.LIST_ID ;           -- and the join

et 2e:

-- 2nd query
SELECT  
    a.LIST_ID
FROM    
      ( SELECT LIST_ID 
        FROM   DBO.PATIENT_LISTS
        WHERE  LIST_ID = @LIST_ID
      ) AS a
    JOIN  
      ( SELECT LIST_ID                    -- the filtered index
        FROM   DBO.PATIENT_LIST_BESPOKE   -- can be used
        WHERE  END_DTTM IS NULL           -- for the subquery
      ) AS b
    ON  a.LIST_ID = b.LIST_ID             -- and the join

UNION ALL

SELECT  
    a.LIST_ID
FROM    
      ( SELECT LIST_ID 
        FROM   DBO.PATIENT_LISTS
        WHERE  LIST_ID = @LIST_ID
      ) AS a
WHERE NOT EXISTS  
      ( SELECT *
        FROM   DBO.PATIENT_LIST_BESPOKE AS b
        WHERE  a.LIST_ID = b.LIST_ID         -- but not for this
      ) ;

Je pense qu'il est maintenant assez évident que pour la 2ème partie de la requête 2nq, l'index filtré ne peut pas être utilisé.


En détail, concernant ces requêtes, il existe 4 types de LIST_IDvaleurs dans le premier tableau:

  • (a) les valeurs qui ont des lignes correspondantes dans le deuxième tableau, toutes avec END_DTTM IS NULL.

  • (b) les valeurs qui ont des lignes correspondantes dans le deuxième tableau, à la fois avec END_DTTM IS NULLet avec END_DTTM IS NOT NULL.

  • (c) les valeurs qui ont des lignes correspondantes dans le deuxième tableau, toutes avec END_DTTM IS NOT NULL.

  • (d) des valeurs qui n'ont pas de lignes correspondantes dans le deuxième tableau.

Maintenant, la 1ère requête renverra toutes les valeurs de type (a) et (b) plusieurs fois (autant qu'elles ont une ligne correspondante dans le deuxième tableau avec END_DTTM IS NULL) et toutes les lignes de type (c) et (d) exactement une fois ( c'est la partie non correspondante de la jointure externe).

La deuxième requête renverra toutes les valeurs de type (a) et (b) plusieurs fois (autant qu'elles ont une ligne correspondante dans le deuxième tableau avec END_DTTM IS NULL) et toutes les lignes de type (d) exactement une fois.
Il ne renverra aucune valeur de type (c) car la jointure trouvera des lignes correspondantes dans la deuxième table (mais celles-ci en auront END_DTTM IS NOT NULL) et elles seront supprimées par la WHEREclause suivante .

ypercubeᵀᴹ
la source