Filtrowany indeks używany tylko wtedy, gdy filtrowana część znajduje się w DOŁĄCZ, a nie GDZIE

10

Utworzyłem filtrowany indeks poniżej, jednak po uruchomieniu 2 zapytań dalej ten indeks jest wykorzystywany tylko do wyszukiwania w pierwszym przykładzie, który zawiera END_DTTM w JOIN, a nie klauzulę where (to jedyna różnica w zapytaniach) . Czy ktoś może wyjaśnić, dlaczego tak się dzieje?

Tworzenie indeksu

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)

Zapytania

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
źródło

Odpowiedzi:

12

Aby optymalizator dopasował predykat do indeksu (filtrowanego lub w inny sposób), predykat musi pojawiać się obok operacji Get w drzewie zapytań logicznych. Aby to ułatwić, predykaty są generalnie umieszczane jak najbliżej liści drzewa logicznego przed rozpoczęciem optymalizacji.

Aby znacznie uprościć, implementacja strategii indeksu fizycznego robi to:

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

Zapytanie, które Cię interesuje, zaczyna się od predykatu nad złączeniem zewnętrznym:

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

Ten kształt nie pasuje do reguły strategii indeksu, ponieważ predykat nie sąsiaduje z Get. Pierwszą częścią odpowiedzi jest to, że przefiltrowane dopasowanie indeksu zakończy się niepowodzeniem, chyba że predykat będzie mógł zostać przepchnięty poza łączenie zewnętrzne.

Druga część polega po prostu na tym, że optymalizator nie zawiera niezbędnej reguły eksploracji, aby przenieść predykat poza połączenie zewnętrzne po zachowanej stronie, ponieważ transformacja jest tak rzadko ważna. Ogólna funkcja optymalizatora polega na tym, że wdrażane są tylko najczęściej przydatne reguły.

W rezultacie dopasowanie przefiltrowanego indeksu w tym przypadku kończy się niepowodzeniem. Żeby było jasne, przepisywanie byłoby poprawne w bardzo konkretnym cytowanym przypadku (drugie zapytanie).

W przypadku pierwszej formy zapytania (z inną semantyką) predykat jest od samego początku powiązany z łączeniem, a logika przesuwania w dół predykatu może przenieść to w niewielkiej odległości do Get, ponieważ nie musi przechodzić obok sprzężenia zewnętrznego, ponieważ wyjaśniono powyżej.

Kontekst i dalsze informacje:

Paul White 9
źródło
9

Nie są to semantycznie te same zapytania, ponieważ można filtrować przed dołączeniem, a następnie filtrować po nim. Pozwól mi zilustrować prostszym przykładem:

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

Zapytanie 1 zwraca wszystkie trzy wiersze:

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;

Zapytanie 2 pomija 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;

Dowód SQLfiddle

Jeśli próbujesz wykonać łączenie anty-semi, testowana kolumna nie może mieć wartości zerowej . Przesuwanie kryteriów między ON i WHERE nie ma żadnej logicznej różnicy, gdy masz do czynienia tylko z połączeniami INNER, ale w przypadku OUTER istnieje znacząca różnica. I powinieneś bardziej dbać o to, by wyniki były prawidłowe, niż o to, czy można nawet użyć filtrowanego indeksu.

Aaron Bertrand
źródło
dziękuję za odpowiedź, ale nie twierdzę, że zapytania są takie same, pytam, dlaczego jedno zapytanie używa przefiltrowanego indeksu, a drugie nie.
Chris
@chris Czy próbowałeś wymusić ten indeks za pomocą podpowiedzi do indeksu? Chciałbym porównać rzeczywiste plany po wykonaniu z tą wskazówką i bez niej. Dla mnie jasne jest, że optymalizator ignoruje ten indeks, gdy uważa, że ​​wykonuje sprzężenie anty-semi (ponieważ nie spodziewałby się, że w takim przypadku zostanie użyta kolumna zerowa), ale nie jestem pewien, czy to zrobić z kosztorysowaniem lub kolejnością operacji lub pewną podstawową wiedzą, że potencjalnie istnieje dużo więcej wierszy wychodzących z lewej strony niż te, które są w przefiltrowanym indeksie. Widok planów może pomóc.
Aaron Bertrand
3

Dwa zapytania są różne - w znaczeniu i wynikach. Oto przepisywanie, więc bardziej oczywiste jest, co robią te dwa zapytania:

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

i 2:

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

Myślę, że teraz jest całkiem oczywiste, że w 2. części zapytania 2nq nie można użyć filtrowanego indeksu.


Szczegółowo, w odniesieniu do tych zapytań, LIST_IDw pierwszej tabeli znajdują się 4 typy wartości:

  • (a) wartości, które mają pasujące wiersze w drugiej tabeli, wszystkie z END_DTTM IS NULL.

  • (b) wartości, które mają pasujące wiersze w drugiej tabeli, zarówno z, jak END_DTTM IS NULLi z END_DTTM IS NOT NULL.

  • (c) wartości, które mają pasujące wiersze w drugiej tabeli, wszystkie z END_DTTM IS NOT NULL.

  • (d) wartości, które nie mają pasujących wierszy w drugiej tabeli.

Teraz pierwsze zapytanie zwróci wszystkie wartości typu (a) i (b) możliwie wiele razy (tyle, ile mają pasujący wiersz w drugiej tabeli z END_DTTM IS NULL) i wszystkie wiersze typu (c) id (d) dokładnie raz ( to niepasująca część złączenia zewnętrznego).

Drugie zapytanie zwróci wszystkie wartości typu (a) i (b) możliwie wiele razy (tyle, ile mają pasujący wiersz w drugiej tabeli END_DTTM IS NULL) i wszystkie wiersze typu (d) dokładnie raz.
Będzie nie zwraca żadnej wartości typu (c), ponieważ sprzężenie znajdzie dopasowania wiersze tabeli drugiej (lecz będą mieć END_DTTM IS NOT NULL) oraz będą one usunięte przez kolejne WHEREpunktu.

ypercubeᵀᴹ
źródło