it-swarm.com.de

Warum CTE (Common Table Expressions) in einigen Fällen Abfragen im Vergleich zu temporären Tabellen in SQL Server verlangsamen

Ich habe mehrere Fälle, in denen meine komplexe CTE (Common Table Expressions) zehnmal langsamer ist als die gleichen Abfragen, die die temporären Tabellen in SQL Server verwenden.

Meine Frage hier ist, wie SQL Server die CTE-Abfragen verarbeitet. Es sieht so aus, als würde sie versuchen, alle getrennten Abfragen zusammenzufügen, anstatt die Ergebnisse jeder einzelnen zu speichern und dann die folgenden auszuführen. Das könnte der Grund sein, warum es bei der Verwendung temporärer Tabellen so schneller ist.

Zum Beispiel:

Abfrage 1 : mit Common Table Expression:

;WITH Orders AS
(
    SELECT
        ma.MasterAccountId,
        IIF(r.FinalisedDate IS NULL, 1, 0)) [Status]
    FROM 
        MasterAccount ma
    INNER JOIN 
        task.tblAccounts a ON a.AccountNumber = ma.TaskAccountId 
                           AND a.IsActive = 1
    LEFT OUTER JOIN 
        task.tblRequisitions r ON r.AccountNumber = a.AccountNumber 
    WHERE 
        ma.IsActive = 1
        AND CAST(r.BatchDateTime AS DATE) BETWEEN @fromDate AND @toDate
        AND r.BatchNumber > 0
),
StockAvailability AS
(
    SELECT sa.AccountNumber,
           sa.RequisitionNumber,
           sa.RequisitionDate,
           sa.Lines,
           sa.HasStock,
           sa.NoStock,
           CASE WHEN sa.Lines = 0 THEN 'Empty'
                WHEN sa.HasStock = 0 THEN 'None'
                WHEN (sa.Lines > 0 AND sa.Lines > sa.HasStock) THEN 'Partial'
                WHEN (sa.Lines > 0 AND sa.Lines <= sa.HasStock) THEN 'Full'
            END AS [Status]
    FROM
    (
        SELECT
                r.AccountNumber,
                r.RequisitionNumber,
                r.RequisitionDate,
                COUNT(rl.ProductNumber) Lines,
                SUM(IIF(ISNULL(psoh.AvailableStock, 0) >= ISNULL(rl.Quantity, 0), 1, 0)) AS HasStock,
                SUM(IIF(ISNULL(psoh.AvailableStock, 0) < ISNULL(rl.Quantity, 0), 1, 0)) AS NoStock

        FROM task.tblrequisitions r 
        INNER JOIN task.tblRequisitionLines rl ON rl.RequisitionNumber = r.RequisitionNumber
        LEFT JOIN ProductStockOnHandSummary psoh ON psoh.ProductNumber = rl.ProductNumber

        WHERE dbo.fn_RemoveUnitPrefix(r.BatchNumber) = 0
          AND r.UnitId = 1
          AND r.FinalisedDate IS NULL
          AND r.RequisitionStatus = 1 
          AND r.TransactionTypeNumber = 301 
        GROUP BY r.AccountNumber, r.RequisitionNumber, r.RequisitionDate
    ) AS sa
),
Available AS
(
    SELECT  ma.MasterAccountId,
            SUM(IIF(ma.IsPartialStock = 1,  CASE WHEN sa.[Status] IN ('Full', 'Partial') THEN 1 ELSE 0 END, 
                                            CASE WHEN sa.[Status] = 'Full' THEN 1 ELSE 0 END)) AS AvailableStock,
            SUM(IIF(sa.[Status] IN ('Full', 'Partial', 'None'), 1, 0))  AS OrdersAnyStock, 

            SUM(IIF(sa.RequisitionDate < dbo.TicksToTime(ma.DailyOrderCutOffTime, @toDate),
                    IIF(ma.IsPartialStock = 1,  CASE WHEN sa.[Status] IN ('Full', 'Partial') THEN 1 ELSE 0 END, 
                                                CASE WHEN sa.[Status] = 'Full' THEN 1 ELSE 0 END), 0)) AS AvailableBeforeCutOff                             
    FROM MasterAccount ma
    INNER JOIN StockAvailability sa ON sa.AccountNumber = ma.TaskAccountId
    GROUP BY ma.MasterAccountId, ma.IsPartialStock
),
Totals AS
(
    SELECT 
        o.MasterAccountId,
        COUNT(o.MasterAccountId) AS BatchedOrders
    FROM Orders o
    GROUP BY o.MasterAccountId
)
SELECT a.MasterAccountId,
       ISNULL(t.BatchedOrders, 0) BatchedOrders,
       ISNULL(t.PendingOrders, 0) PendingOrders,
       ISNULL(av.AvailableStock, 0) AvailableOrders,
       ISNULL(av.AvailableBeforeCutOff, 0) AvailableCutOff,
       ISNULL(av.OrdersAnyStock, 0) AllOrders
FROM MasterAccount a
LEFT OUTER JOIN Available av ON av.MasterAccountId = a.MasterAccountId
LEFT OUTER JOIN Totals t ON t.MasterAccountId = a.MasterAccountId
WHERE a.IsActive = 1

Abfrage 2 : temporäre Tabellen verwenden:

DROP TABLE IF EXISTS #Orders

CREATE TABLE #Orders (MasterAccountId int, [Status] int);

INSERT INTO #Orders
SELECT
    ma.MasterAccountId,
    dbo.fn_GetBatchPickingStatus(ma.BatchPickingOnHold,
                                    iif(r.GroupNumber > 0, 1, 0),
                                    iif(r.FinalisedDate is null, 1, 0)) [Status]
FROM MasterAccount ma (nolock)
INNER JOIN wh3.dbo.tblAccounts a (nolock) on a.AccountNumber = dbo.fn_RemoveUnitPrefix(ma.TaskAccountId) and a.IsActive = 1
LEFT OUTER JOIN wh3.dbo.tblRequisitions r (nolock) on r.AccountNumber = a.AccountNumber 
WHERE cast(r.BatchDateTime as date) between @fromDate and @toDate
    AND r.BatchNumber > 0
    AND ma.IsActive = 1

DROP TABLE IF EXISTS #StockAvailability
Create Table #StockAvailability (AccountNumber int, RequisitionNumber int, RequisitionDate datetime, Lines int, HasStock int, NoStock int);
Insert Into #StockAvailability
SELECT
        r.AccountNumber,
        r.RequisitionNumber,
        r.RequisitionDate,
        COUNT(rl.ProductNumber) Lines,
        SUM(IIF(ISNULL(psoh.AvailableStock, 0) >= ISNULL(rl.Quantity, 0), 1, 0)) AS HasStock,
        SUM(IIF(ISNULL(psoh.AvailableStock, 0) < ISNULL(rl.Quantity, 0), 1, 0)) AS NoStock

FROM WH3.dbo.tblrequisitions r (nolock)
INNER JOIN WH3.dbo.tblRequisitionLines rl (nolock) ON rl.RequisitionNumber = r.RequisitionNumber
LEFT JOIN ProductStockOnHandSummary psoh (nolock) ON psoh.ProductNumber = rl.ProductNumber -- Joined with View          
WHERE r.BatchNumber = 0
    AND r.FinalisedDate is null
    AND r.RequisitionStatus = 1 
    AND r.TransactionTypeNumber = 301 
GROUP BY r.AccountNumber, r.RequisitionNumber, r.RequisitionDate

DROP TABLE IF EXISTS #StockAvailability2
Create Table #StockAvailability2 (AccountNumber int, RequisitionNumber int, RequisitionDate datetime, Lines int, HasStock int, NoStock int, [Status] nvarchar(7));
Insert Into #StockAvailability2
SELECT sa.AccountNumber,
        sa.RequisitionNumber,
        sa.RequisitionDate,
        sa.Lines,
        sa.HasStock,
        sa.NoStock,
        CASE WHEN sa.Lines = 0 THEN 'Empty'
            WHEN sa.HasStock = 0 THEN 'None'
            WHEN (sa.Lines > 0 AND sa.Lines > sa.HasStock) THEN 'Partial'
            WHEN (sa.Lines > 0 AND sa.Lines <= sa.HasStock) THEN 'Full'
        END AS [Status]
FROM #StockAvailability sa

DROP TABLE IF EXISTS #Available
Create Table #Available (MasterAccountId int, AvailableStock int, OrdersAnyStock int, AvailableBeforeCutOff int);
INSERT INTO #Available
SELECT  ma.MasterAccountId,
        SUM(IIF(ma.IsPartialStock = 1,  CASE WHEN sa.[Status] IN ('Full', 'Partial') THEN 1 ELSE 0 END, 
                                        CASE WHEN sa.[Status] = 'Full' THEN 1 ELSE 0 END)) AS AvailableStock,
        SUM(IIF(sa.[Status] IN ('Full', 'Partial', 'None'), 1, 0))  AS OrdersAnyStock, 

        SUM(IIF(sa.RequisitionDate < dbo.TicksToTime(ma.DailyOrderCutOffTime, @toDate),
                IIF(ma.IsPartialStock = 1,  CASE WHEN sa.[Status] IN ('Full', 'Partial') THEN 1 ELSE 0 END, 
                                            CASE WHEN sa.[Status] = 'Full' THEN 1 ELSE 0 END), 0)) AS AvailableBeforeCutOff                             
FROM MasterAccount ma (NOLOCK)
INNER JOIN #StockAvailability2 sa ON sa.AccountNumber = dbo.fn_RemoveUnitPrefix(ma.TaskAccountId)
GROUP BY ma.MasterAccountId, ma.IsPartialStock


;WITH Totals AS
(
    SELECT 
        o.MasterAccountId,
        COUNT(o.MasterAccountId) AS BatchedOrders,
        SUM(IIF(o.[Status] IN (0,1,2), 1, 0)) PendingOrders
    FROM #Orders o (NOLOCK)
    GROUP BY o.MasterAccountId
)
SELECT a.MasterAccountId,
       ISNULL(t.BatchedOrders, 0) BatchedOrders,
       ISNULL(t.PendingOrders, 0) PendingOrders,
       ISNULL(av.AvailableStock, 0) AvailableOrders,
       ISNULL(av.AvailableBeforeCutOff, 0) AvailableCutOff,
       ISNULL(av.OrdersAnyStock, 0) AllOrders
FROM MasterAccount a (NOLOCK)
LEFT OUTER JOIN #Available av (NOLOCK) ON av.MasterAccountId = a.MasterAccountId
LEFT OUTER JOIN Totals t (NOLOCK) ON t.MasterAccountId = a.MasterAccountId
WHERE a.IsActive = 1
12
Roger Oliveira

Die Antwort ist einfach. 

SQL Server verwirklicht keine CTEs. Sie werden inliniert, wie Sie den Ausführungsplänen entnehmen können.

Andere DBMS können dies anders implementieren, ein bekanntes Beispiel ist Postgres, das CTEs verwirklicht (es erstellt im Wesentlichen temporäre Tabellen für CTEs hinter der Motorhaube).

Ob die explizite Materialisierung von Zwischenergebnissen in expliziten temporären Tabellen schneller ist, hängt von der Abfrage ab. 

In komplexen Abfragen kann der Aufwand für das Schreiben und Lesen von Zwischendaten in temporäre Tabellen durch effizientere und einfachere Ausführungspläne kompensiert werden, die der Optimierer generieren kann.

Andererseits ist der CTE in Postgres ein "Optimierungszaun" und die Engine kann Prädikate nicht über die CTE-Grenze schieben.

Manchmal ist ein Weg besser, manchmal ein anderer. Sobald die Abfragekomplexität einen bestimmten Schwellenwert überschreitet, kann ein Optimierer nicht alle Möglichkeiten analysieren, die Daten zu verarbeiten, und er muss sich auf etwas einigen. Beispielsweise die Reihenfolge, in der die Tabellen verbunden werden sollen. Die Anzahl der Permutationen wächst exponentiell mit der Anzahl der Tabellen, aus denen Sie auswählen können. Das Optimierer hat nur begrenzt Zeit, um einen Plan zu erstellen. Daher kann es eine schlechte Entscheidung sein, wenn alle CTEs eingebettet sind. Wenn Sie komplexe Abfragen manuell in kleinere, einfachere zerlegen, müssen Sie wissen, was Sie tun. Der Optimierer hat jedoch eine bessere Chance, für jede einfache Abfrage einen guten Plan zu erstellen.

17

Es gibt verschiedene Anwendungsfälle für beide und verschiedene Vor- und Nachteile.

Allgemeine Tabellenausdrücke

Häufige Tabellenausdrücke sollten als Ausdrücke , nicht Tabellen betrachtet werden. Als Ausdrücke muss der CTE nicht instanziiert werden, sodass das Abfrageoptimierungsprogramm ihn in den Rest der Abfrage einfalten und die Kombination des CTE mit dem Rest der Abfrage optimieren kann.

Temporäre Tabellen

Bei temporären Tabellen werden die Ergebnisse der Abfrage in einer echten Live-Tabelle in der Temp-Datenbank gespeichert. Die Abfrageergebnisse können dann in mehreren Abfragen wiederverwendet werden, im Gegensatz zu CTEs, bei denen der CTE, wenn er in mehreren separaten Abfragen verwendet wird, Teil des Arbeitsplans in jeder dieser separaten Abfragen sein müsste. 

Eine temporäre Tabelle kann auch einen Index, Schlüssel usw. enthalten. Das Hinzufügen einer temporären Tabelle kann eine große Hilfe beim Optimieren einiger Abfragen sein und ist im CTE nicht verfügbar, obwohl der CTE die Indizes und Schlüssel in den zugrunde liegenden Tabellen verwenden kann der CTE.

Wenn die zugrunde liegenden Tabellen für einen CTE nicht die Art von Optimierung unterstützen, die Sie benötigen, kann eine temporäre Tabelle besser sein.

6
rsjaffe

Es kann verschiedene Gründe dafür geben, dass Temp table je nach Abfrage und Anforderung bessere Ergebnisse erzielt als CTE und umgekehrt.

IMO in Ihrem Fall ist sowohl die Abfrage nicht zu optimieren.

Da der CTE jedes Mal ausgewertet wird, wenn er referenziert wird ... so in Ihrem Fall 

SELECT a.MasterAccountId,
       ISNULL(t.BatchedOrders, 0) BatchedOrders,
       ISNULL(t.PendingOrders, 0) PendingOrders,
       ISNULL(av.AvailableStock, 0) AvailableOrders,
       ISNULL(av.AvailableBeforeCutOff, 0) AvailableCutOff,
       ISNULL(av.OrdersAnyStock, 0) AllOrders
FROM MasterAccount a
LEFT OUTER JOIN Available av ON av.MasterAccountId = a.MasterAccountId
LEFT OUTER JOIN Totals t ON t.MasterAccountId = a.MasterAccountId
WHERE a.IsActive = 1

Diese Abfrage zeigt High Cardinality Estimation.MasterAccount-Tabelle wird mehrmals ausgewertet. Aufgrund dieses Grundes ist sie langsam.

Im Falle einer Temp-Tabelle

SELECT a.MasterAccountId,
       ISNULL(t.BatchedOrders, 0) BatchedOrders,
       ISNULL(t.PendingOrders, 0) PendingOrders,
       ISNULL(av.AvailableStock, 0) AvailableOrders,
       ISNULL(av.AvailableBeforeCutOff, 0) AvailableCutOff,
       ISNULL(av.OrdersAnyStock, 0) AllOrders
FROM MasterAccount a (NOLOCK)
LEFT OUTER JOIN #Available av (NOLOCK) ON av.MasterAccountId = a.MasterAccountId
LEFT OUTER JOIN Totals t (NOLOCK) ON t.MasterAccountId = a.MasterAccountId
WHERE a.IsActive = 1

Hier wird #Available bereits ausgewertet, und das Ergebnis wird in der temporären Tabelle gespeichert. Die Variable MasterAccount wird mit der Ergebnismenge Less verknüpft. Daher ist die Kardinalitätsschätzung mit der #Orders-Tabelle ähnlich.

Sowohl die CTE- als auch die Temp-Tabellenabfrage können in Ihrem Fall optimiert werden, wodurch die Leistung verbessert wird.

#Orders sollte Ihre Basistemp-Tabelle sein, und Sie sollten MasterAccount später nicht mehr verwenden. Sie sollten stattdessen #Orders verwenden.

INSERT INTO #Available
SELECT  ma.MasterAccountId,
        SUM(IIF(ma.IsPartialStock = 1,  CASE WHEN sa.[Status] IN ('Full', 'Partial') THEN 1 ELSE 0 END, 
                                        CASE WHEN sa.[Status] = 'Full' THEN 1 ELSE 0 END)) AS AvailableStock,
        SUM(IIF(sa.[Status] IN ('Full', 'Partial', 'None'), 1, 0))  AS OrdersAnyStock, 

        SUM(IIF(sa.RequisitionDate < dbo.TicksToTime(ma.DailyOrderCutOffTime, @toDate),
                IIF(ma.IsPartialStock = 1,  CASE WHEN sa.[Status] IN ('Full', 'Partial') THEN 1 ELSE 0 END, 
                                            CASE WHEN sa.[Status] = 'Full' THEN 1 ELSE 0 END), 0)) AS AvailableBeforeCutOff                             
FROM #Orders ma (NOLOCK)
INNER JOIN #StockAvailability2 sa ON sa.AccountNumber = dbo.fn_RemoveUnitPrefix(ma.TaskAccountId)
GROUP BY ma.MasterAccountId, ma.IsPartialStock

Hier benötigen Sie eine Spalte aus der MasterAcount-Tabelle, z. B. ma.IsPartialStock usw., die sich möglichst in der # order-Tabelle befindet. Hoffentlich ist meine Idee klar.

In der letzten Abfrage ist keine MasterAccount-Tabelle erforderlich

SELECT a.MasterAccountId,
       ISNULL(t.BatchedOrders, 0) BatchedOrders,
       ISNULL(t.PendingOrders, 0) PendingOrders,
       ISNULL(av.AvailableStock, 0) AvailableOrders,
       ISNULL(av.AvailableBeforeCutOff, 0) AvailableCutOff,
       ISNULL(av.OrdersAnyStock, 0) AllOrders
FROM  #Available av 
LEFT OUTER JOIN Totals t  ON t.MasterAccountId = av.MasterAccountId
--WHERE a.IsActive = 1

Ich denke nicht, dass Nolock hint in der temporären Tabelle benötigt wird.

1
KumarHarsh