it-swarm.com.de

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

Ich verwende einen rekursiven CTE für eine Baumstruktur, um alle Nachkommen eines bestimmten Knotens im Baum aufzulisten. Wenn ich einen Literalknotenwert in meine WHERE -Klausel schreibe, scheint SQL Server den CTE tatsächlich nur auf diesen Wert anzuwenden und gibt einen Abfrageplan mit niedrige tatsächliche Zeilenanzahl usw. :

(query plan with literal value

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

(query plan with parameter value

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;
8
binki

Randi Vertongens Antwort beschreibt korrekt, 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 in Antwort bis Verwenden von EXCEPT in einem rekursiven allgemeinen Tabellenausdruck funktioniert.

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

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 - d. H. Es gibt in allen möglichen Fällen genau die gleichen Daten wie die ursprüngliche Abfragespezifikation zurück.

Die Regel, die für das Verschieben von Prädikaten auf einem rekursiven CTE verantwortlich ist, heißt 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 OPTION(QUERYRULEOFF SelOnIterator) deaktiviert werden. Wenn dies verwendet wird, kann der Optimierer keine Prädikate mit einem Literalwert mehr bis zum 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 könnte auch dazu gebracht werden, mit Variablen oder Parametern zu arbeiten, indem OPTION (RECOMPILE) angegeben wird, da dieser Hinweis die Optimierung der Parametereinbettung aktiviert, wobei die Der Laufzeitliteralwert der Variablen (oder des Parameters) wird beim Kompilieren des Plans verwendet. Der Plan wird nicht zwischengespeichert, daher ist der Nachteil eine neue Zusammenstellung bei jeder Ausführung.

Irgendwann wurde die Regel SelOnIterator 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.

12
Paul White 9

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:

  • Verwenden Sie OPTION(RECOMPILE), damit die Filterung für den Literalwert früher erfolgt.
  • 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. (für Ihren Fix nicht erforderlich)

Die Filterung nach @id Wird früher sowohl auf die rekursiven als auch auf die Ankerelemente im Ausführungsplan angewendet, wobei 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 unter 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. Ein Q/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:

(enter image description here

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


OPTION (RECOMPILE)

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

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

(enter image description here

Abfrageplan unter SQL Server 2012 SP4 DDR mit OPTION (RECOMPILE)

10
Randi Vertongen