Gefilterter Index wird nur verwendet, wenn sich der gefilterte Teil im JOIN befindet, nicht im WHERE

10

Ich habe den gefilterten Index unten erstellt. Wenn ich jedoch die 2 Abfragen weiter unten ausführe, wird dieser Index nur für eine Suche im ersten Beispiel verwendet, das END_DTTM in JOIN anstelle der where-Klausel enthält (das ist der einzige Unterschied in den Abfragen). . Kann jemand erklären, warum das passiert?

Indexerstellung

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)

Abfragen

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
quelle

Antworten:

12

Damit der Optimierer ein Prädikat einem Index (gefiltert oder auf andere Weise) zuordnen kann, muss das Prädikat neben der Get-Operation im logischen Abfragebaum angezeigt werden. Um dies zu erleichtern, werden Prädikate im Allgemeinen so nah wie möglich an die Blätter des logischen Baums geschoben, bevor die Optimierung beginnt.

Um dies erheblich zu vereinfachen, führt die Implementierung der physischen Indexstrategie Folgendes aus:

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

Die Abfrage, an der Sie interessiert sind, beginnt mit dem Prädikat über einem äußeren Join:

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

Diese Form entspricht nicht der Indexstrategieregel, da das Prädikat nicht neben Get steht. Der erste Teil der Antwort lautet also, dass der gefilterte Indexabgleich fehlschlägt, es sei denn, das Prädikat kann über den äußeren Join hinaus verschoben werden.

Der zweite Teil besteht einfach darin, dass der Optimierer nicht die erforderliche Explorationsregel enthält, um ein Prädikat an einer äußeren Verknüpfung auf der erhaltenen Seite vorbei zu verschieben, da die Transformation so selten gültig ist. Es ist eine allgemeine Funktion des Optimierers, dass nur die am häufigsten nützlichen Regeln implementiert werden.

Infolgedessen schlägt der Abgleich des gefilterten Index in diesem Fall fehl. Um klar zu sein, wäre das Umschreiben in dem von Ihnen genannten speziellen Fall gültig (zweite Abfrage).

Für das erste Abfrageformular (mit unterschiedlicher Semantik) wird das Prädikat von Anfang an dem Join zugeordnet, und die Prädikat-Pushdown-Logik kann dies um die kurze Strecke zum Get verschieben, da es nicht über einen äußeren Join als hinaus verschoben werden muss oben erklärt.

Hintergrund und weitere Informationen:

Paul White 9
quelle
9

Dies sind semantisch nicht dieselben Abfragen, da einer vor dem Join filtern kann, nach dem der andere filtern kann. Lassen Sie mich anhand eines einfacheren Beispiels veranschaulichen:

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

Abfrage 1 gibt alle drei Zeilen zurück:

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;

Abfrage 2 lässt jedoch LeftyID 2 aus:

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 Beweis

Wenn Sie versuchen, einen Anti-Semi-Join durchzuführen , darf die getestete Spalte nicht nullwertfähig sein . Das Verschieben von Kriterien zwischen ON und WHERE macht keinen logischen Unterschied, wenn Sie nur mit INNER-Joins arbeiten, aber mit OUTER gibt es einen signifikanten Unterschied. Und Sie sollten sich mehr darum kümmern, dass Ihre Ergebnisse korrekt sind, als ob ein gefilterter Index überhaupt verwendet werden könnte oder nicht.

Aaron Bertrand
quelle
Vielen Dank für die Antwort, aber ich behaupte nicht, dass die Abfragen gleich sind. Ich frage, warum eine Abfrage den gefilterten Index verwendet und die andere nicht.
Chris
@chris Haben Sie versucht, diesen Index mit einem Indexhinweis zu erzwingen? Ich wäre neugierig, aktuelle Pläne nach der Ausführung mit und ohne diesen Hinweis zu vergleichen. Für mich ist klar, dass der Optimierer diesen Index ignoriert, wenn er glaubt, dass er einen Anti-Semi-Join ausführt (da er in diesem Fall nicht erwarten würde, dass eine nullfähige Spalte verwendet wird), aber ich bin mir nicht sicher, ob dies der Fall ist Dies hängt mit der Kalkulation oder der Reihenfolge der Operationen oder dem zugrunde liegenden Wissen zusammen, dass möglicherweise viel mehr Zeilen von der linken Seite kommen als die im gefilterten Index. Die Pläne zu sehen könnte helfen.
Aaron Bertrand
3

Die beiden Abfragen sind unterschiedlich - in Bedeutung und Ergebnissen. Hier ist eine Umschreibung, daher ist es offensichtlicher, was die beiden Abfragen tun:

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

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

Ich denke, es ist jetzt ziemlich offensichtlich, dass für den zweiten Teil der 2nq-Abfrage der gefilterte Index nicht verwendet werden kann.


Im Detail gibt es in Bezug auf diese Abfragen 4 Arten von LIST_IDWerten in der ersten Tabelle:

  • (a) Werte mit übereinstimmenden Zeilen in der zweiten Tabelle, alle mit END_DTTM IS NULL.

  • (b) Werte mit übereinstimmenden Zeilen in der zweiten Tabelle, sowohl mit END_DTTM IS NULLals auch mit END_DTTM IS NOT NULL.

  • (c) Werte mit übereinstimmenden Zeilen in der zweiten Tabelle, alle mit END_DTTM IS NOT NULL.

  • (d) Werte, die keine übereinstimmenden Zeilen in der zweiten Tabelle haben.

Jetzt gibt die erste Abfrage alle Werte vom Typ (a) und (b) möglicherweise mehrmals (so viele wie sie eine übereinstimmende Zeile in der zweiten Tabelle mit haben END_DTTM IS NULL) und alle Zeilen vom Typ (c) und (d) genau einmal zurück ( das ist der nicht übereinstimmende Teil des äußeren Joins).

Die zweite Abfrage gibt alle Werte vom Typ (a) und (b) möglicherweise mehrmals (so viele wie sie eine übereinstimmende Zeile in der zweiten Tabelle mit haben END_DTTM IS NULL) und alle Zeilen vom Typ (d) genau einmal zurück.
Es wird kein Wert vom Typ (c) zurückgegeben, da der Join übereinstimmende Zeilen in der zweiten Tabelle findet (diese jedoch haben END_DTTM IS NOT NULL) und diese durch die nachfolgende WHEREKlausel entfernt werden.

ypercubeᵀᴹ
quelle