Eine großartige Ressource zur Berechnung laufender Summen in SQL Server ist dieses Dokument
von Itzik Ben Gan, der dem SQL Server-Team als Teil seiner Kampagne für den OVER
vorgelegt wurde -Klausel gegenüber der ursprünglichen SQL Server 2005-Implementierung weiter erweitert. Darin zeigt er, wie der Cursor, sobald Sie in Zehntausende von Zeilen gelangen, mengenbasierte Lösungen ausführt. SQL Server 2012 hat tatsächlich den OVER
erweitert -Klausel, die diese Art von Abfrage viel einfacher macht.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Da Sie sich auf SQL Server 2005 befinden, steht Ihnen dies jedoch nicht zur Verfügung.
Adam Machanic zeigt hier wie die CLR verwendet werden kann, um die Leistung von Standard-TSQL-Cursorn zu verbessern.
Für diese Tabellendefinition
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Ich erstelle Tabellen mit 2.000 und 10.000 Zeilen in einer Datenbank mit ALLOW_SNAPSHOT_ISOLATION ON
und eine mit dieser Einstellung (Der Grund dafür ist, dass meine anfänglichen Ergebnisse in einer DB mit der Einstellung auf waren, die zu einem rätselhaften Aspekt der Ergebnisse führte).
Die gruppierten Indizes für alle Tabellen hatten nur 1 Stammseite. Die Anzahl der Blattseiten ist unten angegeben.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Ich habe die folgenden Fälle getestet (Links zeigen Ausführungspläne)
- Beitreten links und Gruppieren nach
- Korrelierte Unterabfrage 2000-Zeilenplan ,10000-Zeilen-Plan
- CTE aus Mikaels (aktualisierter) Antwort
- CTE unten
Der Grund für die Einbeziehung der zusätzlichen CTE-Option bestand darin, eine CTE-Lösung bereitzustellen, die auch dann noch funktioniert, wenn ind
Spalte war nicht garantiert sequentiell.
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;
WITH RecursiveCTE
AS (
SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
FROM RunningTotals
ORDER BY ind
UNION ALL
SELECT R.ind, R.col1, R.Total
FROM (
SELECT T.*,
T.col1 + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.ind)
FROM RunningTotals T
JOIN RecursiveCTE R
ON R.ind < T.ind
) R
WHERE R.rn = 1
)
SELECT @col1 =col1, @sumcol1=Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
Alle Abfragen hatten einen CAST(col1 AS BIGINT)
hinzugefügt, um Überlauffehler zur Laufzeit zu vermeiden. Zusätzlich habe ich für alle die Ergebnisse wie oben Variablen zugewiesen, um die Zeit zu eliminieren, die für das Zurücksenden von Ergebnissen aufgewendet wurde.
Ergebnisse
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | Base Table | Work Table | Time |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | Snapshot | Rows | Scan count | logical reads | Scan count | logical reads | cpu | elapsed |
| Group By | On | 2,000 | 2001 | 12709 | | | 1469 | 1250 |
| | On | 10,000 | 10001 | 216678 | | | 30906 | 30963 |
| | Off | 2,000 | 2001 | 9251 | | | 1140 | 1160 |
| | Off | 10,000 | 10001 | 130089 | | | 29906 | 28306 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query | On | 2,000 | 2001 | 12709 | | | 844 | 823 |
| | On | 10,000 | 2 | 82 | 10000 | 165025 | 24672 | 24535 |
| | Off | 2,000 | 2001 | 9251 | | | 766 | 999 |
| | Off | 10,000 | 2 | 48 | 10000 | 165025 | 25188 | 23880 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps | On | 2,000 | 0 | 4002 | 2 | 12001 | 78 | 101 |
| | On | 10,000 | 0 | 20002 | 2 | 60001 | 344 | 342 |
| | Off | 2,000 | 0 | 4002 | 2 | 12001 | 62 | 253 |
| | Off | 10,000 | 0 | 20002 | 2 | 60001 | 281 | 326 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On | 2,000 | 2001 | 4009 | 2 | 12001 | 47 | 75 |
| | On | 10,000 | 10001 | 20040 | 2 | 60001 | 312 | 413 |
| | Off | 2,000 | 2001 | 4006 | 2 | 12001 | 94 | 90 |
| | Off | 10,000 | 10001 | 20023 | 2 | 60001 | 313 | 349 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Sowohl die korrelierte Unterabfrage als auch GROUP BY
Version verwenden "dreieckige" Joins mit verschachtelten Schleifen, die von einem Clustered-Index-Scan auf den RunningTotals
gesteuert werden Tabelle (T1
) und suche für jede von diesem Scan zurückgegebene Zeile zurück in der Tabelle (T2
) Selbstbeitritt auf T2.ind<=T1.ind
.
Dies bedeutet, dass dieselben Zeilen wiederholt verarbeitet werden. Wenn T1.ind=1000
Zeile verarbeitet wird, ruft der Self-Join alle Zeilen mit einem ind <= 1000
ab und summiert sie , dann für die nächste Zeile mit T1.ind=1001
dieselben 1000 Zeilen werden erneut abgerufen und zusammen mit einer zusätzlichen Zeile summiert und so weiter.
Die Gesamtzahl solcher Operationen für eine Tabelle mit 2.000 Zeilen beträgt 2.001.000, für 10.000 Zeilen 50.005.000 oder allgemeiner (n² + n) / 2
die eindeutig exponentiell wächst.
Im Fall von 2.000 Zeilen ist der Hauptunterschied zwischen GROUP BY
und die Unterabfrageversionen ist, dass die erstere das Stream-Aggregat nach dem Join hat und daher drei Spalten enthält (T1.ind
, T2.col1
, T2.col1
) und ein GROUP BY
Eigenschaft von T1.ind
wohingegen letzteres als skalares Aggregat berechnet wird, wobei das Stream-Aggregat vor dem Join nur T2.col1
hat hineinfüttert und kein GROUP BY
hat Eigenschaftssatz überhaupt. Diese einfachere Anordnung hat einen messbaren Vorteil in Form von reduzierter CPU-Zeit.
Für den Fall mit 10.000 Zeilen gibt es einen zusätzlichen Unterschied im Unterabfrageplan. Es fügt eine eifrige Spule
hinzu die alle ind,cast(col1 as bigint)
kopiert Werte in tempdb
. Für den Fall, dass die Snapshot-Isolation aktiviert ist, funktioniert dies kompakter als die Cluster-Indexstruktur, und der Nettoeffekt besteht darin, die Anzahl der Lesevorgänge um etwa 25 % zu reduzieren (da die Basistabelle ziemlich viel leeren Platz für Versionsinformationen behält). Wenn diese Option ausgeschaltet ist, funktioniert es weniger kompakt (vermutlich wegen der bigint
vs int
Unterschied) und es resultieren mehr Lesevorgänge. Dadurch wird die Lücke zwischen der Unterabfrage und den Gruppieren-nach-Versionen verringert, aber die Unterabfrage gewinnt immer noch.
Der klare Gewinner war jedoch der rekursive CTE. Für die "no gaps"-Version sind logische Lesevorgänge aus der Basistabelle jetzt 2 x (n + 1)
was den n
widerspiegelt index sucht im 2-Ebenen-Index, um alle Zeilen plus die zusätzliche am Ende abzurufen, die nichts zurückgibt und die Rekursion beendet. Das bedeutete jedoch immer noch 20.002 Lesevorgänge, um eine 22-Seiten-Tabelle zu verarbeiten!
Logische Arbeitstabellenlesevorgänge für die rekursive CTE-Version sind sehr hoch. Es scheint bei 6 Worktable-Lesevorgängen pro Quellzeile zu funktionieren. Diese stammen aus der Indexspule, die die Ausgabe der vorherigen Zeile speichert und dann in der nächsten Iteration erneut gelesen wird (gute Erklärung dazu von Umachandar Jayachandran hier ). Trotz der hohen Zahl ist dies immer noch der beste Performer.