Database
 sql >> Datenbank >  >> RDS >> Database

Grundlagen von Tabellenausdrücken, Teil 7 – CTEs, Optimierungsbetrachtungen

Dieser Artikel ist der siebte Teil einer Serie über benannte Tabellenausdrücke. In Teil 5 und Teil 6 habe ich die konzeptionellen Aspekte von Common Table Expressions (CTEs) behandelt. In diesem und im nächsten Monat liegt mein Fokus auf Überlegungen zur Optimierung von CTEs.

Ich beginne damit, das Konzept der Entschachtelung benannter Tabellenausdrücke schnell noch einmal zu betrachten und seine Anwendbarkeit auf CTEs zu demonstrieren. Ich werde mich dann auf Persistenzüberlegungen konzentrieren. Ich werde über Persistenzaspekte rekursiver und nichtrekursiver CTEs sprechen. Ich werde erklären, wann es sinnvoll ist, sich an CTEs zu halten und wann es tatsächlich sinnvoller ist, mit temporären Tabellen zu arbeiten.

In meinen Beispielen verwende ich weiterhin die Beispieldatenbanken TSQLV5 und PerformanceV5. Das Skript, das TSQLV5 erstellt und füllt, finden Sie hier und sein ER-Diagramm hier. Das Skript, das PerformanceV5 erstellt und füllt, finden Sie hier.

Ersetzung/Entschachtelung

In Teil 4 der Serie, der sich auf die Optimierung abgeleiteter Tabellen konzentrierte, habe ich einen Prozess der Entschachtelung/Ersetzung von Tabellenausdrücken beschrieben. Ich habe erklärt, dass SQL Server, wenn es eine Abfrage mit abgeleiteten Tabellen optimiert, Transformationsregeln auf den anfänglichen Baum logischer Operatoren anwendet, der vom Parser erzeugt wird, wodurch möglicherweise Dinge über die ursprünglichen Grenzen von Tabellenausdrücken hinweg verschoben werden. Dies geschieht so weit, dass beim Vergleich eines Plans für eine Abfrage mit abgeleiteten Tabellen mit einem Plan für eine Abfrage, die direkt mit den zugrunde liegenden Basistabellen übereinstimmt, auf die Sie selbst die Entschachtelungslogik angewendet haben, sie gleich aussehen. Ich habe auch eine Technik beschrieben, um das Aufheben der Verschachtelung mithilfe des TOP-Filters mit einer sehr großen Anzahl von Zeilen als Eingabe zu verhindern. Ich habe ein paar Fälle demonstriert, in denen diese Technik recht praktisch war – einen, bei dem das Ziel darin bestand, Fehler zu vermeiden, und einen anderen aus Optimierungsgründen.

Die TL;DR-Version des Ersetzens/Entschachtelns von CTEs ist, dass der Prozess derselbe ist wie bei abgeleiteten Tabellen. Wenn Sie mit dieser Aussage zufrieden sind, können Sie diesen Abschnitt gerne überspringen und direkt zum nächsten Abschnitt über Persistenz springen. Sie werden nichts Wichtiges verpassen, das Sie nicht schon einmal gelesen haben. Wenn Sie jedoch wie ich sind, möchten Sie wahrscheinlich einen Beweis dafür, dass dies tatsächlich der Fall ist. Dann möchten Sie wahrscheinlich diesen Abschnitt weiterlesen und den Code testen, den ich verwende, während ich wichtige Beispiele zum Aufheben der Verschachtelung, die ich zuvor mit abgeleiteten Tabellen demonstriert habe, noch einmal durchsuche und sie für die Verwendung von CTEs umwandle.

In Teil 4 habe ich die folgende Abfrage demonstriert (wir nennen sie Abfrage 1):

USE TSQLV5;
 
  SELECT orderid, orderdate
  FROM ( SELECT *
     FROM ( SELECT *
            FROM ( SELECT *
                   FROM Sales.Orders
                   WHERE orderdate >= '20180101' ) AS D1
            WHERE orderdate >= '20180201' ) AS D2
     WHERE orderdate >= '20180301' ) AS D3
  WHERE orderdate >= '20180401';

Die Abfrage umfasst drei Verschachtelungsebenen abgeleiteter Tabellen sowie eine äußere Abfrage. Jede Ebene filtert einen anderen Bereich von Bestelldaten. Der Plan für Abfrage 1 ist in Abbildung 1 dargestellt.

Abbildung 1:Ausführungsplan für Abfrage 1

Der Plan in Abbildung 1 zeigt deutlich, dass die Entschachtelung der abgeleiteten Tabellen stattgefunden hat, da alle Filterprädikate zu einem einzigen umfassenden Filterprädikat zusammengeführt wurden.

Ich habe erklärt, dass Sie den Entschachtelungsprozess verhindern können, indem Sie einen sinnvollen TOP-Filter (im Gegensatz zu TOP 100 PERCENT) mit einer sehr großen Anzahl von Zeilen als Eingabe verwenden, wie die folgende Abfrage zeigt (wir nennen sie Abfrage 2):

  SELECT orderid, orderdate
  FROM ( SELECT TOP (9223372036854775807) *
     FROM ( SELECT TOP (9223372036854775807) *
            FROM ( SELECT TOP (9223372036854775807) *
                   FROM Sales.Orders
                   WHERE orderdate >= '20180101' ) AS D1
            WHERE orderdate >= '20180201' ) AS D2
     WHERE orderdate >= '20180301' ) AS D3
  WHERE orderdate >= '20180401';

Der Plan für Abfrage 2 ist in Abbildung 2 dargestellt.

Abbildung 2:Ausführungsplan für Abfrage 2

Der Plan zeigt deutlich, dass keine Entschachtelung stattgefunden hat, da Sie die abgeleiteten Tabellengrenzen effektiv sehen können.

Versuchen wir die gleichen Beispiele mit CTEs. Hier ist Abfrage 1, die zur Verwendung von CTEs konvertiert wurde:

  WITH C1 AS
  (
    SELECT *
    FROM Sales.Orders
    WHERE orderdate >= '20180101'
  ),
  C2 AS
  (
    SELECT *
    FROM C1
    WHERE orderdate >= '20180201'
  ),
  C3 AS
  (
    SELECT *
    FROM C2
    WHERE orderdate >= '20180301'
  )
  SELECT orderid, orderdate
  FROM C3
  WHERE orderdate >= '20180401';

Sie erhalten genau denselben Plan, der zuvor in Abbildung 1 gezeigt wurde, wo Sie sehen können, dass die Verschachtelung aufgehoben wurde.

Hier ist Abfrage 2, die zur Verwendung von CTEs konvertiert wurde:

  WITH C1 AS
  (
    SELECT TOP (9223372036854775807) *
    FROM Sales.Orders
    WHERE orderdate >= '20180101'
  ),
  C2 AS
  (
    SELECT TOP (9223372036854775807) *
    FROM C1
    WHERE orderdate >= '20180201'
  ),
  C3 AS
  (
    SELECT TOP (9223372036854775807) *
    FROM C2
    WHERE orderdate >= '20180301'
  )
  SELECT orderid, orderdate
  FROM C3
  WHERE orderdate >= '20180401';

Sie erhalten denselben Plan wie zuvor in Abbildung 2 gezeigt, wo Sie sehen können, dass keine Entschachtelung stattgefunden hat.

Sehen wir uns als Nächstes die beiden Beispiele an, die ich verwendet habe, um die praktische Anwendbarkeit der Technik zur Verhinderung des Aufhebens der Verschachtelung zu demonstrieren – nur dieses Mal mit CTEs.

Beginnen wir mit der fehlerhaften Abfrage. Die folgende Abfrage versucht, Bestellpositionen mit einem Rabatt zurückzugeben, der größer als der Mindestrabatt ist und bei dem der Kehrwert des Rabatts größer als 10 ist:

  SELECT orderid, productid, discount
  FROM Sales.OrderDetails
  WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails)
  AND 1.0 / discount > 10.0;

Der Mindestrabatt kann nicht negativ sein, sondern ist entweder null oder höher. Sie denken also wahrscheinlich, dass, wenn eine Zeile einen Rabatt von null hat, das erste Prädikat als falsch ausgewertet werden sollte und dass ein Kurzschluss den Versuch verhindern sollte, das zweite Prädikat auszuwerten, wodurch ein Fehler vermieden wird. Wenn Sie diesen Code ausführen, erhalten Sie jedoch einen Division-durch-Null-Fehler:

  Msg 8134, Level 16, State 1, Line 99
  Divide by zero error encountered.

Das Problem ist, dass, obwohl SQL Server ein Kurzschlusskonzept auf der physischen Verarbeitungsebene unterstützt, es keine Gewissheit gibt, dass es die Filterprädikate in schriftlicher Reihenfolge von links nach rechts auswertet. Ein gängiger Versuch, solche Fehler zu vermeiden, besteht darin, einen benannten Tabellenausdruck zu verwenden, der den Teil der Filterlogik verarbeitet, der zuerst ausgewertet werden soll, und die äußere Abfrage die Filterlogik verarbeiten zu lassen, die als zweites ausgewertet werden soll. Hier ist der Lösungsversuch mit einem CTE:

  WITH C AS
  (
    SELECT *
    FROM Sales.OrderDetails
    WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails)
  )
  SELECT orderid, productid, discount
  FROM C
  WHERE 1.0 / discount > 10.0;

Leider führt das Aufheben der Verschachtelung des Tabellenausdrucks zu einem logischen Äquivalent zur ursprünglichen Lösungsabfrage, und wenn Sie versuchen, diesen Code auszuführen, erhalten Sie erneut einen Division-durch-Null-Fehler:

  Msg 8134, Level 16, State 1, Line 108
  Divide by zero error encountered.

Mit unserem Trick mit dem TOP-Filter in der inneren Abfrage verhindern Sie das Entschachteln des Tabellenausdrucks, etwa so:

  WITH C AS
  (
    SELECT TOP (9223372036854775807) *
    FROM Sales.OrderDetails
    WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails)
  )
  SELECT orderid, productid, discount
  FROM C
  WHERE 1.0 / discount > 10.0;

Diesmal läuft der Code erfolgreich ohne Fehler.

Fahren wir mit dem Beispiel fort, in dem Sie die Technik verwenden, um das Aufheben der Verschachtelung aus Optimierungsgründen zu verhindern. Der folgende Code gibt nur Versender zurück, deren maximales Bestelldatum am oder nach dem 1. Januar 2018 liegt:

  USE PerformanceV5; 
 
  WITH C AS
  (
    SELECT S.shipperid,
      (SELECT MAX(O.orderdate)
       FROM dbo.Orders AS O
       WHERE O.shipperid = S.shipperid) AS maxod
    FROM dbo.Shippers AS S
  )
  SELECT shipperid, maxod
  FROM C
  WHERE maxod >= '20180101';

Wenn Sie sich fragen, warum Sie nicht eine viel einfachere Lösung mit einer gruppierten Abfrage und einem HAVING-Filter verwenden sollten, hat dies mit der Dichte der shipperid-Spalte zu tun. Die Orders-Tabelle enthält 1.000.000 Bestellungen, und die Sendungen dieser Bestellungen wurden von fünf Versendern abgewickelt, was bedeutet, dass jeder Versender im Durchschnitt 20 % der Bestellungen abgewickelt hat. Der Plan für eine gruppierte Abfrage, die das maximale Bestelldatum pro Versender berechnet, würde alle 1.000.000 Zeilen scannen, was zu Tausenden von Seitenlesevorgängen führen würde. Wenn Sie nur die innere Abfrage des CTE hervorheben (wir nennen sie Abfrage 3), die das maximale Bestelldatum pro Versender berechnet, und ihren Ausführungsplan überprüfen, erhalten Sie den in Abbildung 3 gezeigten Plan.

Abbildung 3:Ausführungsplan für Abfrage 3

Der Plan scannt fünf Zeilen im gruppierten Index für Versender. Pro Versender wendet der Plan eine Suche auf einen abdeckenden Index für Bestellungen an, wobei (shipperid, orderdate) die indexführenden Schlüssel sind, und geht direkt zur letzten Zeile in jedem Versenderabschnitt auf Blattebene, um das maximale Bestelldatum für den aktuellen abzurufen Absender. Da wir nur fünf Versender haben, gibt es nur fünf Indexsuchoperationen, was zu einem sehr effizienten Plan führt. Hier sind die Leistungsmessungen, die ich erhalten habe, als ich die innere Abfrage des CTE ausgeführt habe:

  duration: 0 ms, CPU: 0 ms, reads: 15

Wenn Sie jedoch die vollständige Lösung ausführen (wir nennen sie Abfrage 4), erhalten Sie einen völlig anderen Plan, wie in Abbildung 4 gezeigt.

Abbildung 4:Ausführungsplan für Abfrage 4

Was geschah, war, dass SQL Server den Tabellenausdruck entschachtelte und die Lösung in ein logisches Äquivalent einer gruppierten Abfrage konvertierte, was zu einem vollständigen Scan des Indexes für Bestellungen führte. Hier sind die Leistungszahlen, die ich für diese Lösung erhalten habe:

  duration: 316 ms, CPU: 281 ms, reads: 3854

Was wir hier brauchen, ist, zu verhindern, dass die Verschachtelung des Tabellenausdrucks stattfindet, sodass die innere Abfrage mit Suchvorgängen gegen den Index für Bestellungen optimiert wird und die äußere Abfrage nur zu einer Hinzufügung eines Filteroperators in der führt planen. Sie erreichen dies mit unserem Trick, indem Sie der inneren Abfrage einen TOP-Filter hinzufügen, etwa so (wir nennen diese Lösung Abfrage 5):

  WITH C AS
  (
    SELECT TOP (9223372036854775807) S.shipperid,
      (SELECT MAX(O.orderdate)
       FROM dbo.Orders AS O
       WHERE O.shipperid = S.shipperid) AS maxod
    FROM dbo.Shippers AS S
  )
  SELECT shipperid, maxod
  FROM C
  WHERE maxod >= '20180101';

Der Plan für diese Lösung ist in Abbildung 5 dargestellt.

Abbildung 5:Ausführungsplan für Abfrage 5

Der Plan zeigt, dass die gewünschte Wirkung erzielt wurde, und dementsprechend bestätigen die Leistungszahlen dies:

  duration: 0 ms, CPU: 0 ms, reads: 15

Unsere Tests bestätigen also, dass SQL Server das Ersetzen/Entschachteln von CTEs genauso handhabt wie bei abgeleiteten Tabellen. Das bedeutet, dass Sie nicht aus Optimierungsgründen einen dem anderen vorziehen sollten, sondern aufgrund von konzeptionellen Unterschieden, die Ihnen wichtig sind, wie in Teil 5 erläutert.

Persistenz

Ein weit verbreitetes Missverständnis in Bezug auf CTEs und benannte Tabellenausdrücke im Allgemeinen ist, dass sie als eine Art Persistenzvehikel dienen. Einige denken, dass SQL Server die Ergebnismenge der inneren Abfrage in einer Arbeitstabelle speichert und dass die äußere Abfrage tatsächlich mit dieser Arbeitstabelle interagiert. In der Praxis werden reguläre nichtrekursive CTEs und abgeleitete Tabellen nicht beibehalten. Ich habe die Entschachtelungslogik beschrieben, die SQL Server beim Optimieren einer Abfrage mit Tabellenausdrücken anwendet, was zu einem Plan führt, der direkt mit den zugrunde liegenden Basistabellen interagiert. Beachten Sie, dass der Optimierer Arbeitstabellen verwenden kann, um Zwischenergebnissätze beizubehalten, wenn dies aus Leistungsgründen oder aus anderen Gründen sinnvoll ist, z. B. zum Schutz vor Halloween. Wenn dies der Fall ist, sehen Sie Spool- oder Index-Spool-Operatoren im Plan. Solche Auswahlmöglichkeiten haben jedoch nichts mit der Verwendung von Tabellenausdrücken in der Abfrage zu tun.

Rekursive CTEs

Es gibt ein paar Ausnahmen, in denen SQL Server die Daten des Tabellenausdrucks beibehält. Eine davon ist die Verwendung von indizierten Ansichten. Wenn Sie einen gruppierten Index für eine Ansicht erstellen, speichert SQL Server das Resultset der inneren Abfrage im gruppierten Index der Ansicht und hält es mit allen Änderungen in den zugrunde liegenden Basistabellen synchron. Die andere Ausnahme ist, wenn Sie rekursive Abfragen verwenden. SQL Server muss die Zwischenergebnismengen der Anker- und rekursiven Abfragen in einer Spule beibehalten, damit es bei jeder Ausführung des rekursiven Members auf die Ergebnismenge der letzten Runde zugreifen kann, die durch die rekursive Referenz auf den CTE-Namen dargestellt wird.

Um dies zu demonstrieren, verwende ich eine der rekursiven Abfragen aus Teil 6 der Serie.

Verwenden Sie den folgenden Code, um die Employees-Tabelle in der tempdb-Datenbank zu erstellen, sie mit Beispieldaten zu füllen und einen unterstützenden Index zu erstellen:

  SET NOCOUNT ON;
 
  USE tempdb;
 
  DROP TABLE IF EXISTS dbo.Employees;
  GO
 
  CREATE TABLE dbo.Employees
  (
    empid   INT         NOT NULL
    CONSTRAINT PK_Employees PRIMARY KEY,
    mgrid   INT         NULL     
    CONSTRAINT FK_Employees_Employees REFERENCES dbo.Employees,
    empname VARCHAR(25) NOT NULL,
    salary  MONEY       NOT NULL,
    CHECK (empid <> mgrid)
  );
 
  INSERT INTO dbo.Employees(empid, mgrid, empname, salary)
  VALUES(1,  NULL, 'David'  , $10000.00),
        (2,     1, 'Eitan'  ,  $7000.00),
        (3,     1, 'Ina'    ,  $7500.00),
        (4,     2, 'Seraph' ,  $5000.00),
        (5,     2, 'Jiru'   ,  $5500.00),
        (6,     2, 'Steve'  ,  $4500.00),
        (7,     3, 'Aaron'  ,  $5000.00),
        (8,     5, 'Lilach' ,  $3500.00),
        (9,     7, 'Rita'   ,  $3000.00),
        (10,    5, 'Sean'   ,  $3000.00),
        (11,    7, 'Gabriel',  $3000.00),
        (12,    9, 'Emilia' ,  $2000.00),
        (13,    9, 'Michael',  $2000.00),
        (14,    9, 'Didi'   ,  $1500.00);
 
  CREATE UNIQUE INDEX idx_unc_mgrid_empid
  ON dbo.Employees(mgrid, empid)
  INCLUDE(empname, salary);
  GO

Ich habe den folgenden rekursiven CTE verwendet, um alle Untergebenen eines Wurzelmanagers eines Eingabeunterbaums zurückzugeben, wobei in diesem Beispiel Mitarbeiter 3 als Eingabemanager verwendet wurde:

DECLARE @root AS INT = 3;
 
  WITH C AS
  (
    SELECT empid, mgrid, empname
    FROM dbo.Employees
    WHERE empid = @root
    UNION ALL
    SELECT S.empid, S.mgrid, S.empname
    FROM C AS M
      INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
  )
  SELECT empid, mgrid, empname
  FROM C;

Der Plan für diese Abfrage (wir nennen sie Abfrage 6) ist in Abbildung 6 dargestellt.

Abbildung 6:Ausführungsplan für Abfrage 6

Beachten Sie, dass das Allererste, was im Plan passiert, rechts vom Root-SELECT-Knoten, die Erstellung einer B-Baum-basierten Arbeitstabelle ist, die durch den Index-Spool-Operator dargestellt wird. Der obere Teil des Plans behandelt die Logik des Ankermitglieds. Es zieht die Eingabemitarbeiterzeilen aus dem gruppierten Index für Mitarbeiter und schreibt sie in die Spule. Der untere Teil des Plans repräsentiert die Logik des rekursiven Members. Es wird wiederholt ausgeführt, bis es eine leere Ergebnismenge zurückgibt. Die äußere Eingabe für den Nested-Loops-Operator erhält die Manager aus der vorherigen Runde aus der Spule (Table Spool-Operator). Die innere Eingabe verwendet einen Index Seek-Operator für einen nicht gruppierten Index, der auf Employees (mgrid, empid) erstellt wurde, um die direkten Untergebenen der Manager aus der vorherigen Runde abzurufen. Die Ergebnismenge jeder Ausführung des unteren Teils des Plans wird ebenfalls in die Indexspule geschrieben. Beachten Sie, dass insgesamt 7 Zeilen in die Spule geschrieben wurden. Einer wird vom Anker-Member zurückgegeben und 6 weitere werden von allen Ausführungen des rekursiven Members zurückgegeben.

Abgesehen davon ist es interessant festzustellen, wie der Plan mit dem standardmäßigen maxrecursion-Limit von 100 umgeht. Beachten Sie, dass der untere Compute Scalar-Operator einen internen Zähler mit dem Namen „Expr1011“ bei jeder Ausführung des rekursiven Members um 1 erhöht. Dann setzt der Assert-Operator ein Flag auf Null, wenn dieser Zähler 100 überschreitet. Wenn dies geschieht, stoppt SQL Server die Ausführung der Abfrage und generiert einen Fehler.

Wann nicht bestehen bleiben

Zurück zu nichtrekursiven CTEs, die normalerweise nicht persistiert werden, müssen Sie aus Optimierungssicht herausfinden, wann es besser ist, sie im Vergleich zu tatsächlichen Persistenztools wie temporären Tabellen und Tabellenvariablen zu verwenden. Ich werde ein paar Beispiele durchgehen, um zu zeigen, wann welcher Ansatz optimaler ist.

Beginnen wir mit einem Beispiel, bei dem CTEs besser abschneiden als temporäre Tabellen. Das ist oft der Fall, wenn Sie nicht mehrere Auswertungen desselben CTE haben, sondern vielleicht nur eine modulare Lösung, bei der jeder CTE nur einmal ausgewertet wird. Der folgende Code (wir nennen ihn Abfrage 7) fragt die Orders-Tabelle in der Performance-Datenbank ab, die 1.000.000 Zeilen enthält, um Bestelljahre zurückzugeben, in denen mehr als 70 verschiedene Kunden Bestellungen aufgegeben haben:

  USE PerformanceV5;
 
  WITH C1 AS
  (
    SELECT YEAR(orderdate) AS orderyear, custid
    FROM dbo.Orders
  ),
  C2 AS
  (
    SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
    FROM C1
    GROUP BY orderyear
  )
  SELECT orderyear, numcusts
  FROM C2
  WHERE numcusts > 70;

Diese Abfrage generiert die folgende Ausgabe:

  orderyear   numcusts
  ----------- -----------
  2015        992
  2017        20000
  2018        20000
  2019        20000
  2016        20000

Ich habe diesen Code mit SQL Server 2019 Developer Edition ausgeführt und den in Abbildung 7 gezeigten Plan erhalten.

Abbildung 7:Ausführungsplan für Abfrage 7

Beachten Sie, dass das Aufheben der Verschachtelung des CTE zu einem Plan führte, der die Daten aus einem Index in der Orders-Tabelle abruft und kein Spooling des inneren Abfrageergebnissatzes des CTE beinhaltet. Beim Ausführen dieser Abfrage auf meinem Computer habe ich die folgenden Leistungszahlen erhalten:

  duration: 265 ms, CPU: 828 ms, reads: 3970, writes: 0

Lassen Sie uns nun eine Lösung ausprobieren, die temporäre Tabellen anstelle von CTEs verwendet (wir nennen sie Lösung 8), etwa so:

  SELECT YEAR(orderdate) AS orderyear, custid
  INTO #T1
  FROM dbo.Orders;
 
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  INTO #T2
  FROM #T1
  GROUP BY orderyear;
 
  SELECT orderyear, numcusts
  FROM #T2
  WHERE numcusts > 70;
 
  DROP TABLE #T1, #T2;

Die Pläne für diese Lösung sind in Abbildung 8 dargestellt.

Abbildung 8:Pläne für Lösung 8

Beachten Sie, dass die Operatoren zum Einfügen von Tabellen die Ergebnismengen in die temporären Tabellen #T1 und #T2 schreiben. Der erste ist besonders teuer, da er 1.000.000 Zeilen in #T1 schreibt. Hier sind die Leistungszahlen, die ich für diese Hinrichtung erhalten habe:

  duration: 454 ms, CPU: 1517 ms, reads: 14359, writes: 359

Wie Sie sehen, ist die Lösung mit den CTEs viel optimaler.

Wann bestehen bleiben

Ist also eine modulare Lösung mit nur einer Auswertung jedes CTE immer einer temporären Tabelle vorzuziehen? Nicht unbedingt. Bei CTE-basierten Lösungen, die viele Schritte umfassen und zu ausgefeilten Plänen führen, bei denen der Optimierer viele Kardinalitätsschätzungen an vielen verschiedenen Punkten im Plan anwenden muss, könnten Sie mit akkumulierten Ungenauigkeiten enden, die zu suboptimalen Entscheidungen führen. Eine der Techniken, um zu versuchen, solche Fälle anzugehen, besteht darin, einige Zwischenergebnissätze selbst in temporären Tabellen zu speichern und bei Bedarf sogar Indizes dafür zu erstellen, was dem Optimierer einen Neuanfang mit neuen Statistiken ermöglicht und die Wahrscheinlichkeit für Kardinalitätsschätzungen von besserer Qualität erhöht hoffentlich zu optimaleren Entscheidungen führen. Ob dies besser ist als eine Lösung, die keine temporären Tabellen verwendet, müssen Sie testen. Manchmal lohnt es sich, zusätzliche Kosten für die Beibehaltung von Zwischenergebnissen zugunsten besserer Kardinalitätsschätzungen in Kauf zu nehmen.

Ein weiterer typischer Fall, in dem die Verwendung temporärer Tabellen der bevorzugte Ansatz ist, ist, wenn die CTE-basierte Lösung mehrere Auswertungen desselben CTE hat und die innere Abfrage des CTE ziemlich teuer ist. Betrachten Sie die folgende CTE-basierte Lösung (wir nennen sie Abfrage 9), die jedem Bestelljahr und -monat ein anderes Bestelljahr und einen anderen Bestellmonat mit der engsten Bestellanzahl zuordnet:

  WITH OrdCount AS
  (
    SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth,
      COUNT(*) AS numorders
    FROM dbo.Orders
    GROUP BY YEAR(orderdate), MONTH(orderdate)
  )
  SELECT O1.orderyear, O1.ordermonth, O1.numorders,
    O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2,
    O2.numorders AS numorders2
  FROM OrdCount AS O1 
  CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders
                FROM OrdCount AS O2 
                WHERE O2.orderyear <> O1.orderyear
                  OR O2.ordermonth <> O1.ordermonth
                ORDER BY ABS(O1.numorders - O2.numorders),
                  O2.orderyear, O2.ordermonth ) AS O2;

Diese Abfrage generiert die folgende Ausgabe:

  orderyear   ordermonth  numorders   orderyear2  ordermonth2 numorders2
  ----------- ----------- ----------- ----------- ----------- -----------
  2016        1           21262       2017        3           21267
  2019        1           21227       2016        5           21229
  2019        2           19145       2018        2           19125
  2018        4           20561       2016        9           20554
  2018        5           21209       2019        5           21210
  2018        6           20515       2016        11          20513
  2018        7           21194       2018        10          21197
  2017        9           20542       2017        11          20539
  2017        10          21234       2019        3           21235
  2017        11          20539       2019        4           20537
  2017        12          21183       2016        8           21185
  2018        1           21241       2019        7           21238
  2016        2           19844       2019        12          20184
  2018        3           21222       2016        10          21222
  2016        4           20526       2019        9           20527
  2019        4           20537       2017        11          20539
  2017        5           21203       2017        8           21199
  2019        6           20531       2019        9           20527
  2017        7           21217       2016        7           21218
  2018        8           21283       2017        3           21267
  2018        10          21197       2017        8           21199
  2016        11          20513       2018        6           20515
  2019        11          20494       2017        4           20498
  2018        2           19125       2019        2           19145
  2016        3           21211       2016        12          21212
  2019        3           21235       2017        10          21234
  2016        5           21229       2019        1           21227
  2019        5           21210       2016        3           21211
  2017        6           20551       2016        9           20554
  2017        8           21199       2018        10          21197
  2018        9           20487       2019        11          20494
  2016        10          21222       2018        3           21222
  2018        11          20575       2016        6           20571
  2016        12          21212       2016        3           21211
  2019        12          20184       2018        9           20487
  2017        1           21223       2016        10          21222
  2017        2           19174       2019        2           19145
  2017        3           21267       2016        1           21262
  2017        4           20498       2019        11          20494
  2016        6           20571       2018        11          20575
  2016        7           21218       2017        7           21217
  2019        7           21238       2018        1           21241
  2016        8           21185       2017        12          21183
  2019        8           21189       2016        8           21185
  2016        9           20554       2017        6           20551
  2019        9           20527       2016        4           20526
  2019        10          21254       2016        1           21262
  2015        12          1018        2018        2           19125
  2018        12          21225       2017        1           21223

  (49 rows affected)

Der Plan für Abfrage 9 ist in Abbildung 9 dargestellt.

Abbildung 9:Ausführungsplan für Abfrage 9

Der obere Teil des Plans entspricht der Instanz des OrdCount-CTE mit dem Alias ​​O1. Diese Referenz führt zu einer Auswertung des CTE OrdCount. Dieser Teil des Plans ruft die Zeilen aus einem Index der Orders-Tabelle ab, gruppiert sie nach Jahr und Monat und aggregiert die Anzahl der Bestellungen pro Gruppe, was zu 49 Zeilen führt. Der untere Teil des Plans entspricht der korrelierten abgeleiteten Tabelle O2, die pro Zeile von O1 angewendet wird, also 49 Mal ausgeführt wird. Jede Ausführung fragt den OrdCount CTE ab und führt daher zu einer separaten Auswertung der inneren Abfrage des CTE. Sie können sehen, dass der untere Teil des Plans alle Zeilen aus dem Index auf Bestellungen durchsucht, gruppiert und aggregiert. Sie erhalten im Grunde insgesamt 50 Auswertungen des CTE, was dazu führt, dass die 1.000.000 Zeilen von Bestellungen 50 Mal gescannt, gruppiert und aggregiert werden. Das klingt nicht nach einer sehr effizienten Lösung. Hier sind die Leistungsmessungen, die ich beim Ausführen dieser Lösung auf meinem Computer erhalten habe:

  duration: 16 seconds, CPU: 56 seconds, reads: 130404, writes: 0

Angesichts der Tatsache, dass nur ein paar Dutzend Monate involviert sind, wäre es viel effizienter, eine temporäre Tabelle zu verwenden, um das Ergebnis einer einzelnen Aktivität zu speichern, die die Zeilen von Orders gruppiert und aggregiert, und dann sowohl die äußeren als auch die inneren Eingaben davon zu haben der APPLY-Operator interagiert mit der temporären Tabelle. Hier ist die Lösung (wir nennen sie Lösung 10) mit einer temporären Tabelle anstelle des CTE:

  SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth,
    COUNT(*) AS numorders
  INTO #OrdCount
  FROM dbo.Orders
  GROUP BY YEAR(orderdate), MONTH(orderdate);
 
  SELECT O1.orderyear, O1.ordermonth, O1.numorders,
    O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2,
    O2.numorders AS numorders2
  FROM #OrdCount AS O1
  CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders
                FROM #OrdCount AS O2
                WHERE O2.orderyear <> O1.orderyear 
                  OR O2.ordermonth <> O1.ordermonth
                ORDER BY ABS(O1.numorders - O2.numorders),
                  O2.orderyear, O2.ordermonth ) AS O2;
 
  DROP TABLE #OrdCount;

Hier macht es wenig Sinn, die temporäre Tabelle zu indizieren, da der TOP-Filter in seiner Ordnungsangabe auf einer Berechnung basiert und somit eine Sortierung unumgänglich ist. Es kann aber durchaus sein, dass es in anderen Fällen, bei anderen Lösungen, auch für Sie relevant wäre, eine Indexierung Ihrer temporären Tabellen in Erwägung zu ziehen. Jedenfalls ist der Plan für diese Lösung in Abbildung 10 dargestellt.

Abbildung 10:Ausführungspläne für Lösung 10

Beobachten Sie im oberen Plan, wie das schwere Heben, das das Scannen von 1.000.000 Zeilen, deren Gruppierung und Aggregation umfasst, nur einmal erfolgt. 49 Zeilen werden in die temporäre Tabelle #OrdCount geschrieben, und dann interagiert der untere Plan mit der temporären Tabelle für die äußeren und inneren Eingaben des Nested Loops-Operators, der die Logik des APPLY-Operators handhabt.

Hier sind die Leistungszahlen, die ich für die Ausführung dieser Lösung erhalten habe:

  duration: 0.392 seconds, CPU: 0.5 seconds, reads: 3636, writes: 3

Es ist um Größenordnungen schneller als die CTE-basierte Lösung.

Was kommt als nächstes?

In diesem Artikel habe ich mit der Behandlung von Optimierungsüberlegungen im Zusammenhang mit CTEs begonnen. Ich habe gezeigt, dass der Entschachtelungs-/Substitutionsprozess, der bei abgeleiteten Tabellen stattfindet, bei CTEs genauso funktioniert. Ich habe auch die Tatsache besprochen, dass nichtrekursive CTEs nicht persistiert werden, und erklärt, dass Sie, wenn Persistenz ein wichtiger Faktor für die Leistung Ihrer Lösung ist, selbst damit umgehen müssen, indem Sie Tools wie temporäre Tabellen und Tabellenvariablen verwenden. Nächsten Monat werde ich die Diskussion fortsetzen, indem ich zusätzliche Aspekte der CTE-Optimierung behandle.