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
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.
Es gibt verschiedene Anwendungsfälle für beide und verschiedene Vor- und Nachteile.
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.
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.
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.