Warum verwendet dieser rekursive CTE mit einem Parameter keinen Index, wenn er mit einem Literal arbeitet?

8

Ich verwende einen rekursiven CTE für eine Baumstruktur, um alle Nachkommen eines bestimmten Knotens im Baum aufzulisten. Wenn ich einen Literalknotenwert in meine WHEREKlausel schreibe , scheint SQL Server den CTE tatsächlich nur auf diesen Wert anzuwenden, wodurch ein Abfrageplan mit geringen tatsächlichen Zeilenzahlen usw. erstellt wird :

Abfrageplan mit Literalwert

Wenn ich jedoch den Wert als Parameter übergebe, scheint er den CTE zu realisieren (zu spulen) und ihn dann nachträglich zu filtern :

Abfrageplan mit Parameterwert

Ich könnte die Pläne falsch lesen. Ich habe kein Leistungsproblem bemerkt, befürchte jedoch, dass die Realisierung des CTE Probleme mit größeren Datenmengen verursachen könnte, insbesondere in einem geschäftigeren System. Außerdem addiere ich diese Durchquerung normalerweise auf sich selbst: Ich gehe zu Vorfahren und zurück zu Nachkommen (um sicherzustellen, dass ich alle zugehörigen Knoten sammle). Aufgrund meiner Daten ist jeder Satz „verwandter“ Knoten eher klein, sodass die Realisierung des CTE keinen Sinn ergibt. Und wenn SQL Server den CTE zu erkennen scheint, gibt es mir einige ziemlich große Zahlen in seinen "tatsächlichen" Zählungen.

Gibt es eine Möglichkeit, die parametrisierte Version der Abfrage so zu gestalten, dass sie sich wie die Literalversion verhält? Ich möchte den CTE wiederverwendbar machen.

Abfrage mit Literal:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Abfrage mit Parameter:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Setup-Code:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;
Binki
quelle

Antworten:

12

In der Antwort von Randi Vertongen geht es richtig darum, wie Sie mit der parametrisierten Version der Abfrage den gewünschten Plan erhalten können. Diese Antwort ergänzt dies, indem Sie den Titel der Frage ansprechen, falls Sie an den Details interessiert sind.

SQL Server schreibt endrekursive Common Table-Ausdrücke (CTEs) als Iteration neu. Alles von der Lazy Index Spool bis hinunter ist die Laufzeitimplementierung der iterativen Übersetzung. Ich habe einen detaillierten Bericht darüber geschrieben, wie dieser Abschnitt eines Ausführungsplans als Antwort auf die Verwendung von EXCEPT in einem rekursiven allgemeinen Tabellenausdruck funktioniert .

Sie möchten ein Prädikat (Filter) außerhalb des CTE angeben und das Abfrageoptimierungsprogramm diesen Filter innerhalb der Rekursion nach unten drücken (als Iteration umgeschrieben) und auf das Ankerelement anwenden lassen. Dies bedeutet, dass die Rekursion nur mit den übereinstimmenden Datensätzen beginnt ParentId = @Id.

Dies ist eine vernünftige Erwartung, unabhängig davon, ob ein Literalwert, eine Variable oder ein Parameter verwendet wird. Der Optimierer kann jedoch nur Dinge tun, für die Regeln geschrieben wurden. Regeln geben an, wie ein logischer Abfragebaum geändert wird, um eine bestimmte Transformation zu erreichen. Sie enthalten Logik, um sicherzustellen, dass das Endergebnis sicher ist - dh, dass in allen möglichen Fällen genau dieselben Daten wie die ursprüngliche Abfragespezifikation zurückgegeben werden.

Die Regel, die für das Verschieben von Prädikaten auf einem rekursiven CTE verantwortlich ist, wird aufgerufen SelOnIterator- eine relationale Auswahl (= Prädikat) auf einem Iterator, der die Rekursion implementiert. Genauer gesagt kann diese Regel eine Auswahl in den Ankerteil der rekursiven Iteration kopieren :

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Diese Regel kann mit dem undokumentierten Hinweis deaktiviert werden OPTION(QUERYRULEOFF SelOnIterator). Wenn dies verwendet wird, kann der Optimierer keine Prädikate mit einem Literalwert mehr auf den Anker eines rekursiven CTE verschieben. Sie wollen das nicht, aber es veranschaulicht den Punkt.

Ursprünglich beschränkte sich diese Regel darauf, nur an Prädikaten mit Literalwerten zu arbeiten. Es kann auch durch Angabe dazu gebracht werden OPTION (RECOMPILE), mit Variablen oder Parametern zu arbeiten , da dieser Hinweis die Parametereinbettungsoptimierung aktiviert , wobei der Laufzeitliteralwert der Variablen (oder des Parameters) beim Kompilieren des Plans verwendet wird. Der Plan wird nicht zwischengespeichert, daher ist der Nachteil eine neue Zusammenstellung bei jeder Ausführung.

Irgendwann wurde die SelOnIteratorRegel verbessert, um auch mit Variablen und Parametern zu arbeiten. Um unerwartete Planänderungen zu vermeiden, wurde dies unter dem Ablaufverfolgungsflag 4199, der Datenbankkompatibilitätsstufe und der Hotfix-Kompatibilitätsstufe des Abfrageoptimierers geschützt. Dies ist ein ganz normales Muster für Optimierungsverbesserungen, die nicht immer dokumentiert sind. Verbesserungen sind normalerweise für die meisten Menschen gut, aber es besteht immer die Möglichkeit, dass eine Änderung zu einer Regression für jemanden führt.

Ich möchte den CTE wiederverwendbar machen

Sie können anstelle einer Ansicht auch eine Inline-Tabellenwertfunktion verwenden. Geben Sie den Wert an, den Sie als Parameter nach unten drücken möchten, und platzieren Sie das Prädikat im rekursiven Ankerelement.

Wenn Sie möchten, können Sie auch das Trace-Flag 4199 global aktivieren. Es gibt viele Optimierungsänderungen, die von diesem Flag abgedeckt werden. Sie müssen daher Ihre Arbeitslast sorgfältig testen, wenn sie aktiviert ist, und auf Regressionen vorbereitet sein.

Paul White 9
quelle
10

Obwohl ich momentan nicht den Titel des eigentlichen Hotfixes habe, wird der bessere Abfrageplan verwendet, wenn die Hotfixes für das Abfrageoptimierungsprogramm in Ihrer Version (SQL Server 2012) aktiviert werden.

Einige andere Methoden sind:

  • Wenn Sie OPTION(RECOMPILE)dies verwenden, erfolgt die Filterung früher für den Literalwert.
  • Unter SQL Server 2016 oder höher werden die Hotfixes vor dieser Version automatisch angewendet, und die Abfrage sollte auch dem besseren Ausführungsplan entsprechen.

Hotfixes für das Abfrageoptimierungsprogramm

Sie können diese Korrekturen mit aktivieren

  • Traceflag 4199 vor SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; ab SQL Server 2016. (wird für Ihr Update nicht benötigt)

Die Filterung @idwird früher auf die rekursiven und Ankerelemente im Ausführungsplan angewendet, wenn der Hotfix aktiviert ist.

Das Traceflag kann auf Abfrageebene hinzugefügt werden:

OPTION(QUERYTRACEON 4199)

Wenn Sie die Abfrage unter SQL Server 2012 SP4 DDR oder SQL Server 2014 SP3 mit Traceflag 4199 ausführen, wird der bessere Abfrageplan ausgewählt:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

Abfrageplan unter SQL Server 2014 SP3 mit Traceflag 4199

Abfrageplan für SQL Server 2012 SP4 DDR mit Traceflag 4199

Abfrageplan unter SQL Server 2012 SP4 DDR ohne Traceflag 4199

Der Hauptkonsens besteht darin, das Traceflag 4199 global zu aktivieren, wenn eine Version vor SQL Server 2016 verwendet wird. Anschließend kann diskutiert werden, ob es aktiviert werden soll oder nicht. AQ / A dazu hier .


Kompatibilitätsstufe 130 oder 140

Beim Testen der parametrisierten Abfrage in einer Datenbank mit compatibility_level= 130 oder 140 erfolgt die Filterung früher:

Geben Sie hier die Bildbeschreibung ein

Aufgrund der Tatsache, dass die 'alten' Fixes von Traceflag 4199 unter SQL Server 2016 und höher aktiviert sind.


OPTION (REKOMPILIEREN)

Obwohl eine Prozedur verwendet wird, kann SQL Server beim Hinzufügen nach dem Literalwert filtern OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

Geben Sie hier die Bildbeschreibung ein

Abfrageplan für SQL Server 2012 SP4 DDR mit OPTION (RECOMPILE)

Randi Vertongen
quelle