Der Index für persistierte berechnete Spalten benötigt eine Schlüsselsuche, um die Spalten im berechneten Ausdruck abzurufen

24

Ich habe eine dauerhaft berechnete Spalte in einer Tabelle, die einfach aus verketteten Spalten besteht, z

CREATE TABLE dbo.T 
(   
    ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
    A VARCHAR(20) NOT NULL,
    B VARCHAR(20) NOT NULL,
    C VARCHAR(20) NOT NULL,
    D DATE NULL,
    E VARCHAR(20) NULL,
    Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL 
);

Dies Compist nicht eindeutig, und D ist das Gültigkeitsdatum jeder Kombination von A, B, C. Daher verwende ich die folgende Abfrage, um das Enddatum für jede Kombination zu ermitteln A, B, C(im Grunde genommen das nächste Startdatum für denselben Wert von Comp):

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1
WHERE   t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;

Ich habe dann einen Index zur berechneten Spalte hinzugefügt, um diese Abfrage (und auch andere) zu unterstützen:

CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;

Der Abfrageplan hat mich jedoch überrascht. Ich hätte gedacht, dass der Index für die berechnete Spalte zum Scannen von t1 und t2 verwendet werden könnte , da ich eine WHERE-Klausel habe, die das angibt, D IS NOT NULLund ich sortiere nach Compund verweise nicht auf eine Spalte außerhalb des Index, aber ich sah einen Clustered-Index Scan.

Bildbeschreibung hier eingeben

Daher habe ich die Verwendung dieses Index erzwungen, um festzustellen, ob sich ein besserer Plan ergibt:

SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 t2.D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY t2.D
            )
FROM    dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;

Welches gab diesen Plan

Bildbeschreibung hier eingeben

Dies zeigt, dass eine Schlüsselsuche verwendet wird, deren Details:

Bildbeschreibung hier eingeben

Nun laut der SQL-Server Dokumentation:

Sie können einen Index für eine berechnete Spalte erstellen, die mit einem deterministischen, aber ungenauen Ausdruck definiert ist, wenn die Spalte in der Anweisung CREATE TABLE oder ALTER TABLE als PERSISTED gekennzeichnet ist. Dies bedeutet, dass das Datenbankmodul die berechneten Werte in der Tabelle speichert und aktualisiert, wenn andere Spalten, von denen die berechnete Spalte abhängt, aktualisiert werden. Das Datenbankmodul verwendet diese dauerhaften Werte, wenn es einen Index für die Spalte erstellt und wenn in einer Abfrage auf den Index verwiesen wird. Mit dieser Option können Sie einen Index für eine berechnete Spalte erstellen, wenn das Datenbankmodul nicht genau nachweisen kann, ob eine Funktion, die berechnete Spaltenausdrücke zurückgibt, insbesondere eine in .NET Framework erstellte CLR-Funktion, sowohl deterministisch als auch präzise ist.

Wenn also, wie in den Dokumenten angegeben, "das Datenbankmodul die berechneten Werte in der Tabelle speichert" und der Wert auch in meinem Index gespeichert wird, warum ist eine Schlüsselsuche erforderlich, um A, B und C abzurufen, wenn auf sie nicht verwiesen wird die Abfrage überhaupt? Ich gehe davon aus, dass sie zur Berechnung von Comp verwendet werden, aber warum? Warum kann die Abfrage den Index auch verwenden t2, aber nicht t1?

Abfragen und DDL auf SQL Fiddle

NB Ich habe SQL Server 2008 mit Tags versehen, da dies die Version ist, auf der sich mein Hauptproblem befindet, aber ich erhalte auch 2012 das gleiche Verhalten.

GarethD
quelle

Antworten:

20

Warum ist eine Schlüsselsuche erforderlich, um A, B und C abzurufen, wenn in der Abfrage überhaupt nicht auf sie verwiesen wird? Ich gehe davon aus, dass sie zur Berechnung von Comp verwendet werden, aber warum?

Spalten A, B, and C werden im Abfrageplan referenziert - sie werden von der Suche verwendet T2.

Warum kann die Abfrage den Index für t2 aber nicht für t1 verwenden?

Das Optimierungsprogramm entschied, dass das Durchsuchen des Clustered-Index billiger war als das Durchsuchen des gefilterten Nonclustered-Index und das anschließende Durchsuchen, um die Werte für die Spalten A, B und C abzurufen.

Erläuterung

Die eigentliche Frage ist, warum der Optimierer das Bedürfnis verspürte, A, B und C für die Indexsuche überhaupt abzurufen. Wir erwarten, dass es die CompSpalte mit einem Nonclustered-Index-Scan liest und dann einen Suchvorgang für denselben Index (Alias ​​T2) ausführt, um den Top-1-Datensatz zu finden.

Das Abfrageoptimierungsprogramm erweitert die berechneten Spaltenverweise vor Beginn der Optimierung, um die Kosten verschiedener Abfragepläne bewerten zu können. Bei einigen Abfragen kann der Optimierer durch Erweitern der Definition einer berechneten Spalte effizientere Pläne finden.

Wenn das Optimierungsprogramm auf eine korrelierte Unterabfrage stößt, versucht es, diese in ein Formular umzuwandeln, über das es leichter nachdenken kann. Wenn keine effektivere Vereinfachung gefunden werden kann, wird die korrelierte Unterabfrage als Apply (eine korrelierte Verknüpfung) neu geschrieben:

Neu schreiben anwenden

Nur so kommt es vor, dass das Auflösen der Anwendung den logischen Abfragestruktur in ein Formular versetzt, das mit der Projektnormalisierung nicht gut funktioniert (eine spätere Phase, in der unter anderem allgemeine Ausdrücke mit berechneten Spalten abgeglichen werden sollen).

In Ihrem Fall interagiert die Art und Weise, in der die Abfrage geschrieben wird, mit den internen Details des Optimierers, sodass die Definition des erweiterten Ausdrucks nicht mit der berechneten Spalte abgeglichen A, B, and Cwird Comp. Dies ist die Grundursache.

Umgehung

Eine Möglichkeit, diesen Nebeneffekt zu umgehen, besteht darin, die Abfrage manuell als Apply zu schreiben:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Leider wird diese Abfrage den gefilterten Index nicht verwenden, wie wir auch hoffen würden. Der Ungleichheitstest für die Spalte Din der Apply-Anweisung wird zurückgewiesen NULLs, sodass das anscheinend redundante Vergleichselement WHERE T1.D IS NOT NULLentfernt wird.

Ohne dieses explizite Prädikat entscheidet die Übereinstimmungslogik für gefilterte Indizes, dass der gefilterte Index nicht verwendet werden kann. Es gibt eine Reihe von Möglichkeiten, um diesen zweiten Nebeneffekt zu umgehen. Am einfachsten ist es jedoch, das Kreuz in ein äußeres zu ändern (dies spiegelt die Logik des Umschreibens des Optimierers wider, der zuvor für die korrelierte Unterabfrage ausgeführt wurde):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Jetzt muss das Optimierungsprogramm nicht mehr das Überschreiben der Anwendung selbst verwenden (die berechnete Spaltenübereinstimmung funktioniert also wie erwartet), und das Vergleichselement wird auch nicht entfernt, sodass der gefilterte Index für beide Datenzugriffsvorgänge verwendet werden kann und die Suche die CompSpalte verwendet auf beiden Seiten:

Outer Apply-Plan

Dies ist im Allgemeinen dem Hinzufügen von A, B und C als INCLUDEdSpalten im gefilterten Index vorzuziehen , da hierdurch die Hauptursache des Problems behoben wird und keine unnötige Erweiterung des Index erforderlich ist.

Bestehende berechnete Spalten

Als Randnotiz ist es nicht erforderlich, die berechnete Spalte als zu markieren PERSISTED, wenn es Ihnen nichts ausmacht, die Definition in einer CHECKEinschränkung zu wiederholen :

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    A varchar(20) NOT NULL,
    B varchar(20) NOT NULL,
    C varchar(20) NOT NULL,
    D date NULL,
    E varchar(20) NULL,
    Comp AS A + '-' + B + '-' + C,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

Die berechnete Spalte muss PERSISTEDin diesem Fall nur vorhanden sein, wenn Sie eine NOT NULLEinschränkung verwenden oder die CompSpalte direkt in einer CHECKEinschränkung referenzieren möchten (anstatt ihre Definition zu wiederholen) .

Paul White sagt GoFundMonica
quelle
2
Übrigens, ich bin auf einen anderen Fall von überflüssigem Nachschlagen gestoßen, als ich mir das ansah, das Sie vielleicht interessieren (oder auch nicht). SQL-Geige .
Martin Smith
@MartinSmith Ja das ist interessant. Eine andere generische Regel rewrite ( FOJNtoLSJNandLASJN), die dazu führt, dass die Dinge nicht wie erhofft funktionieren und Müll (BaseRow / Checksums) verbleibt, der in einigen Arten von Plänen (z. B. Cursorn) nützlich ist, aber hier nicht benötigt wird.
Paul White sagt GoFundMonica
Ah Chkist Prüfsumme! Danke, da war ich mir nicht sicher. Ursprünglich dachte ich, es könnte etwas mit Prüfbeschränkungen zu tun haben.
Martin Smith
6

Obwohl dies aufgrund der künstlichen Natur Ihrer Testdaten ein Zufall sein könnte, habe ich, wie Sie bereits erwähnt haben, versucht, Folgendes umzuschreiben:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

Dies ergab einen guten, kostengünstigen Plan, der Ihren Index verwendete und deutlich niedrigere Lesewerte aufwies als die anderen Optionen (und dieselben Ergebnisse für Ihre Testdaten).

Plan Explorer kostet für vier Optionen: Original;  Original mit Andeutung;  äußere anwenden und führen

Ich vermute, Ihre realen Daten sind komplizierter, daher kann es einige Szenarien geben, in denen sich diese Abfrage semantisch von Ihrer unterscheidet, aber es zeigt sich manchmal, dass die neuen Funktionen einen echten Unterschied bewirken können.

Ich habe mit einigen abwechslungsreicheren Daten experimentiert und einige passende Szenarien gefunden, andere nicht:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'
wBob
quelle
1
Naja es benutzt den Index aber nur bis zu einem gewissen Punkt. Wenn compes sich nicht um eine berechnete Spalte handelt, wird die Sortierung nicht angezeigt.
Martin Smith
Vielen Dank. Mein tatsächliches Szenario ist nicht viel komplizierter und die LEADFunktion hat genau so funktioniert, wie ich es mir für meine lokale Instanz von 2012 Express gewünscht hätte. Leider war diese kleine Unannehmlichkeit für mich noch kein guter Grund, die Produktionsserver zu aktualisieren ...
GarethD
-1

Als ich versuchte, die gleichen Aktionen auszuführen, erhielt ich die anderen Ergebnisse. Erstens sieht mein Ausführungsplan für eine Tabelle ohne Indizes folgendermaßen aus:Bildbeschreibung hier eingeben

Wie aus dem Clustered Index Scan (t2) hervorgeht, wird das Prädikat verwendet, um die erforderlichen zurückzugebenden Zeilen zu bestimmen (bedingt durch die Bedingung):

Bildbeschreibung hier eingeben

Wenn der Index hinzugefügt wurde, unabhängig davon, ob er vom WITH-Operator definiert wurde oder nicht, lautete der Ausführungsplan wie folgt:

Bildbeschreibung hier eingeben

Wie wir sehen können, wird der Clustered Index Scan durch den Index Scan ersetzt. Wie wir oben gesehen haben, verwendet der SQL Server die Quellenspalten der berechneten Spalte, um den Abgleich der verschachtelten Abfrage durchzuführen. Während des Clustered-Index-Scan können alle diese Werte gleichzeitig erfasst werden (keine zusätzlichen Vorgänge erforderlich). Wenn der Index hinzugefügt wurde, wird die Filterung der erforderlichen Zeilen aus der Tabelle (in der Hauptauswahl) entsprechend dem Index ausgeführt, aber die Werte der Quellenspalten für die berechnete Spalte compmüssen noch abgerufen werden (letzte Operation, verschachtelte Schleife). .

Bildbeschreibung hier eingeben

Aus diesem Grund wird die Key Lookup-Operation verwendet, um die Daten der Quellenspalten der berechneten abzurufen.

PS Sieht aus wie ein Fehler in SQL Server.

Sandr
quelle