Warum profitiert meine WHERE-Klausel von einer "eingeschlossenen" Spalte?

12

Laut dieser Antwort profitiert die Abfrage nicht von einem Index, es sei denn, ein Index wird über die Spalten erstellt, die zum Einschränken verwendet werden.

Ich habe diese Definition:

CREATE TABLE [dbo].[JobItems] (
    [ItemId]             UNIQUEIDENTIFIER NOT NULL,
    [ItemState]          INT              NOT NULL,
    [ItemPriority]       INT NOT NULL,
    [CreationTime]       DATETIME         NULL DEFAULT GETUTCDATE(),
    [LastAccessTime]     DATETIME         NULL DEFAULT GETUTCDATE(),
     -- other columns
 );

 CREATE UNIQUE CLUSTERED INDEX [JobItemsIndex]
    ON [dbo].[JobItems]([ItemId] ASC);
 GO

CREATE INDEX [GetItemToProcessIndex]
    ON [dbo].[JobItems]([ItemState], [ItemPriority], [CreationTime])
    INCLUDE (LastAccessTime);
GO

und diese Abfrage:

UPDATE TOP (150) JobItems 
SET ItemState = 17 
WHERE 
    ItemState IN (3, 9, 10)
    AND LastAccessTime < DATEADD (day, -2, GETUTCDATE()) 
    AND CreationTime < DATEADD (day, -2, GETUTCDATE());

Ich habe den tatsächlichen Plan überprüft, und es gibt nur eine Indexsuche mit dem Prädikat genau wie in WHERE- keine zusätzlichen "Lesezeichensuchen", die abgerufen werden müssen LastAccessTime, obwohl letztere nur im Index "enthalten" sind und nicht Teil des Index.

Es sieht für mich so aus, als ob dieses Verhalten der Regel widerspricht, dass die Spalte Teil des Index sein muss und nicht nur "enthalten" ist.

Ist das Verhalten, das ich beobachte, das richtige? Wie kann ich im Voraus feststellen, ob meine WHERELeistungen von einer eingeschlossenen Spalte profitieren oder ob die Spalte Teil des Index sein muss?

scharfer Zahn
quelle
Es kann immer noch basierend auf dem ItemStateWert suchen , aber die Suche wird nicht so effizient sein, als ob Ihr Index wie folgt strukturiert wäre(ItemState, CreationTime, LastAccessTime)
Mark Sinkinson
1
@ MarkSinkinson oder einfach(ItemState, CreationTime) INCLUDE (LastAccessTime)
ypercubeᵀᴹ
@sharptooth die verknüpfte Antwort, die Sie haben, sagt das nicht ("es sei denn, ein Index wird über die Spalten erstellt, die zum Einschränken der Abfrage verwendet werden, profitiert von einem Index nicht"). Es heißt, ein Index für (a,b)ist nicht das beste für eine Abfrage mit SELECT a FROM t WHERE b=5;und ein Index für (b) INCLUDE (a)ist viel besser.
Ypercubeᵀᴹ

Antworten:

9

Ihr Prädikat unterscheidet sich von Ihrem Suchprädikat.

Ein Suchprädikat wird verwendet, um die geordneten Daten im Index zu durchsuchen. In diesem Fall werden drei Suchvorgänge ausgeführt, einer für jeden ItemState, an dem Sie interessiert sind. Darüber hinaus befinden sich die Daten in der ItemPriority-Reihenfolge, sodass keine weiteren Suchvorgänge ausgeführt werden können.

Bevor die Daten zurückgegeben werden, überprüft sie jede Zeile mit dem Prädikat, das ich als Residual-Prädikat bezeichne. Es basiert auf den Ergebnissen des Seek Predicate.

Jede eingeschlossene Spalte ist nicht Teil der geordneten Daten, kann jedoch verwendet werden, um das Residual-Prädikat zu erfüllen, ohne die zusätzliche Suche durchführen zu müssen.

Sie können Material sehen, das ich über Sargability geschrieben habe. Suchen Sie insbesondere bei SQLBits unter http://bit.ly/Sargability nach einer Sitzung

Bearbeiten: Um die Auswirkung von Residuen besser darzustellen, führen Sie die Abfrage mit dem undokumentierten OPTION (QUERYTRACEON 9130)Operator aus, der die Residuen in einen separaten Filteroperator aufteilt (der eigentlich eine frühere Version des Plans ist, bevor die Residuen in den Suchoperator verschoben werden). Es zeigt deutlich die Auswirkung eines ineffektiven Suchvorgangs, indem die Anzahl der Zeilen, die an den Filter übergeben werden, angegeben wird.

Es ist auch erwähnenswert, dass die übergebenen Daten aufgrund der IN-Klausel in ItemState tatsächlich in ItemState-Reihenfolge und nicht in ItemPriority-Reihenfolge vorliegen. Ein zusammengesetzter Index für ItemState, gefolgt von einem der Daten (z. B. ItemState, LastAccessTime), kann verwendet werden, um drei Suchvorgänge (Beachten Sie, dass das Suchprädikat drei Suchvorgänge innerhalb eines Suchoperators anzeigt) zu erstellen, die jeweils zwei Ebenen umfassen und Daten erzeugen noch in ItemState-Reihenfolge (z. B. ItemState = 3 und LastAccessTime kleiner als etwas, dann ItemState = 9 und LastAccessTime kleiner als etwas und dann ItemState = 10 und LastAccessTime kleiner als etwas).

Ein Index für (ItemState, LastAccesTime, CreationTime) wäre nicht nützlicher als einer für (ItemState, LastAccessTime), da die CreationTime-Ebene nur dann nützlich ist, wenn Ihre Suche für eine bestimmte ItemState- und LastAccessTime-Kombination gilt, nicht für einen Bereich. Zum Beispiel, dass das Telefonbuch nicht in der Reihenfolge Vorname aufgeführt ist, wenn Sie sich für Nachnamen interessieren, die mit F beginnen.

Wenn Sie einen zusammengesetzten Index möchten, die späteren Spalten in Seek Predicates jedoch aufgrund der Art und Weise, wie Sie die früheren Spalten verwenden, niemals verwenden können, können Sie sie auch als eingebundene Spalten haben, in denen sie weniger Platz in Anspruch nehmen index (weil sie nur auf der Blattebene des Index gespeichert sind, nicht auf der höheren Ebene), aber dennoch Lookups vermeiden und in Residual-Prädikaten verwendet werden können.

Gemäß dem Begriff Residual Predicate - das ist mein eigener Begriff für diese Eigenschaft eines Seeks. Ein Merge-Join nennt es explizit das Äquivalent eines Residual-Prädikats und das Hash-Match ein Probe-Residual (das Sie möglicherweise von TSA erhalten, wenn Sie eine Übereinstimmung für Hash finden). Aber in einem Seek nennen sie es einfach Predicate, was es weniger schlimm erscheinen lässt als es ist.

Rob Farley
quelle
3

GetItemToProcessIndex ist nicht vollständig suchbar, da Ihre where-Klausel aktiviert ist ItemState + LastAccessTime + CreationTime. Die indizierten Spalten und die where-Klausel stimmen nicht perfekt überein.

Wenn Sie einen Deckungsindex ItemState + LastAccessTime + CreationTimefür erstellen, erhalten Sie für jede Übereinstimmung, die Sie von GetItemToProcessIndex erhalten, auch den Wert Ihres Primärschlüssels (ItemId). Es muss nur sichergestellt werden, dass das 2. Datum ein Match ist.

Dies ist alles, was Sie brauchen, um zur Position der Zeile auf ihrer Seite zu springen und sie zu aktualisieren.

Mit Ihrem aktuellen Index kann es dem Server helfen, Zeilen mit dem gewünschten ItemState zu finden, aber es muss dann noch alle aus dem Index lesen, um korrekte Übereinstimmungen in LastAccessTime + CreationTime zu finden. Abhängig von den Datumsprädikaten und der Größe des übereinstimmenden Satzes und dem, was ausgeschlossen werden muss, kann dies zu einem viel größeren IO führen als zu einem perfekt abdeckenden Index für die drei Spalten, bei denen nur ItemState und die zweite Spalte gesucht werden (1. indiziertes Datum). . Das zweite Datum im Index kann jedoch angegeben werden. Zusätzliche Spalten sollten nicht zwischen diesen 3 indiziert werden, obwohl dies als vierte Spalte in Ordnung sein könnte (siehe Robs Antwort zu zusätzlichen Spalten).

Julien Vavasseur
quelle