Indice filtrato usato solo quando la parte filtrata è nel JOIN e non WHERE


10

Ho creato l'indice filtrato di seguito, tuttavia, quando eseguo le 2 query più in basso, questo indice viene utilizzato solo per una ricerca nel primo esempio che ha END_DTTM nel JOIN anziché la clausola where (questa è l'unica differenza nelle query) . Qualcuno può spiegare perché questo accade?

Creazione dell'indice

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)

Interrogazioni

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   

Risposte:


12

Affinché l'ottimizzatore corrisponda a un predicato a un indice (filtrato o meno), il predicato deve apparire adiacente all'operazione Get nella struttura della query logica. Per facilitare ciò, i predicati vengono generalmente spinti il ​​più vicino possibile alle foglie dell'albero logico prima che inizi l'ottimizzazione.

Per semplificare notevolmente, l'implementazione della strategia di indice fisico fa questo:

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

La query a cui sei interessato inizia con il predicato sopra un join esterno:

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

Questa forma non corrisponde alla regola della strategia dell'indice perché il predicato non è adiacente al Get. Quindi, la prima parte della risposta è che la corrispondenza dell'indice filtrato fallirà a meno che il predicato non possa essere spinto oltre il join esterno.

La seconda parte è semplicemente che l'ottimizzatore non contiene la regola di esplorazione necessaria per spostare un predicato oltre un join esterno sul lato conservato, poiché la trasformazione è così raramente valida. È una caratteristica generale dell'ottimizzatore che vengono implementate solo le regole più frequentemente utili.

Di conseguenza, in questo caso la corrispondenza dell'indice filtrato non riesce. Per essere chiari, la riscrittura sarebbe valida nel caso molto specifico che citi (seconda query).

Per il primo modulo di query (con diversa semantica), il predicato è associato al join dall'inizio e la logica di push-down del predicato può spostarlo a breve distanza su Get perché non deve passare oltre un join esterno come spiegato sopra.

Contesto e ulteriori informazioni:


9

Queste non sono semanticamente le stesse query, poiché una può filtrare prima del join e l'altra può filtrare dopo. Vorrei illustrare con un esempio più semplice:

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 query 1 restituisce tutte e tre le righe:

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 query 2, tuttavia, esclude 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;

SQLfiddle proof

Se si sta tentando di eseguire un join anti-semi, la colonna testata non deve essere nulla . Lo spostamento dei criteri tra ON e WHERE non fa alcuna differenza logica quando si ha a che fare solo con i join INNER, ma con OUTER c'è una differenza significativa. E dovresti preoccuparti di più che i tuoi risultati siano corretti rispetto al fatto che un indice filtrato possa essere usato o meno.


grazie per la risposta ma non sto affermando che le query sono uguali, chiedo perché una query utilizza l'indice filtrato e l'altra no.
chris,

@chris Hai provato a forzare quell'indice con un suggerimento sull'indice? Sarei curioso di confrontare i piani reali post-esecuzione con e senza quel suggerimento. Per me è chiaro che l'ottimizzatore sta ignorando quell'indice quando ritiene che stia facendo un join anti-semi (dal momento che non si aspetterebbe di utilizzare una colonna nullable in quel caso), ma non sono sicuro che sia fare con i costi o l'ordine delle operazioni o alcune conoscenze di base che potenzialmente ci sono molte più righe provenienti dal lato sinistro rispetto a quelle che si trovano nell'indice filtrato. Vedere i piani potrebbe aiutare.
Aaron Bertrand

3

Le due query sono diverse: per significato e risultati. Ecco una riscrittura, quindi è più ovvio cosa stanno facendo le due query:

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

e 2o:

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

Penso che ora sia abbastanza ovvio che per la seconda parte della query 2nq, l'indice filtrato non possa essere utilizzato.


Nel dettaglio, per quanto riguarda queste query, ci sono 4 tipi di LIST_IDvalori nella prima tabella:

  • (a) valori con righe corrispondenti nella seconda tabella, tutte con END_DTTM IS NULL.

  • (b) valori con righe corrispondenti nella seconda tabella, sia con END_DTTM IS NULLche con END_DTTM IS NOT NULL.

  • (c) valori con righe corrispondenti nella seconda tabella, tutte con END_DTTM IS NOT NULL.

  • (d) valori che non hanno righe corrispondenti nella seconda tabella.

Ora, la prima query restituirà tutti i valori di tipo (a) e (b) possibilmente molte volte (tante quante hanno una riga corrispondente nella seconda tabella con END_DTTM IS NULL) e tutte le righe di tipo (c) e (d) esattamente una volta ( questa è la parte non corrispondente del join esterno).

La seconda query restituirà tutti i valori di tipo (a) e (b) possibilmente molte volte (quante ne hanno una riga corrispondente nella seconda tabella con END_DTTM IS NULL) e tutte le righe di tipo (d) esattamente una volta.
Esso non restituire qualsiasi valore di tipo (c) perché il join troverà righe corrispondenti nella seconda tabella (ma questi avranno END_DTTM IS NOT NULL) e sarà rimosso dalla successiva WHEREclausola.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.