Een geweldige bron voor het berekenen van lopende totalen in SQL Server is dit document
door Itzik Ben Gan die werd ingediend bij het SQL Server-team als onderdeel van zijn campagne om de OVER
clausule verder uitgebreid van de oorspronkelijke implementatie van SQL Server 2005. Daarin laat hij zien hoe, als je eenmaal in tienduizenden rijen bent gekomen, cursors op set gebaseerde oplossingen uitvoeren. SQL Server 2012 heeft inderdaad de OVER
. uitgebreid clausule die dit soort zoekopdrachten veel gemakkelijker maakt.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Aangezien u SQL Server 2005 gebruikt, is dit echter niet voor u beschikbaar.
Adam Machanic wordt hier weergegeven hoe de CLR kan worden gebruikt om de prestaties van standaard TSQL-cursors te verbeteren.
Voor deze tabeldefinitie
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Ik maak tabellen met zowel 2.000 als 10.000 rijen in een database met ALLOW_SNAPSHOT_ISOLATION ON
en een met deze instelling uit (de reden hiervoor is omdat mijn eerste resultaten in een DB waren met de instelling op die leidde tot een raadselachtig aspect van de resultaten).
De geclusterde indexen voor alle tabellen hadden slechts 1 hoofdpagina. Het aantal bladpagina's voor elk wordt hieronder weergegeven.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Ik heb de volgende gevallen getest (Links tonen uitvoeringsplannen)
- Links lid worden en groeperen op
- Gecorreleerde subquery 2000 rijenplan ,10000 rijenplan
- CTE van Mikael's (bijgewerkte) antwoord
- CTE hieronder
De reden voor het opnemen van de extra CTE-optie was om een CTE-oplossing te bieden die nog steeds zou werken als de ind
kolom was niet gegarandeerd sequentieel.
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 zoekopdrachten hadden een CAST(col1 AS BIGINT)
toegevoegd om overloopfouten tijdens runtime te voorkomen. Bovendien heb ik voor al deze resultaten de resultaten toegewezen aan variabelen zoals hierboven om de tijd te elimineren die besteed wordt aan het terugsturen van resultaten.
Resultaten
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | 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 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Zowel de gecorreleerde subquery als de GROUP BY
versie gebruik "driehoekige" geneste lus-joins aangedreven door een geclusterde indexscan op de RunningTotals
tabel (T1
) en, voor elke rij die door die scan wordt geretourneerd, terug in de tabel zoeken (T2
) zelf lid worden op T2.ind<=T1.ind
.
Dit betekent dat dezelfde rijen herhaaldelijk worden verwerkt. Wanneer de T1.ind=1000
rij wordt verwerkt, de self-join haalt alle rijen op en telt ze op met een ind <= 1000
, dan voor de volgende rij waar T1.ind=1001
dezelfde 1000 rijen worden opnieuw opgehaald en opgeteld met een extra rij enzovoort.
Het totale aantal van dergelijke bewerkingen voor een tabel met 2.000 rijen is 2.001.000, voor 10.000 rijen 50.000.000 of meer in het algemeen (n² + n) / 2
die duidelijk exponentieel groeit.
In het geval van 2000 rijen is het belangrijkste verschil tussen de GROUP BY
en de subqueryversie is dat de eerste de stream-aggregaat heeft na de join en dus drie kolommen heeft die erin worden ingevoerd (T1.ind
, T2.col1
, T2.col1
) en een GROUP BY
eigendom van T1.ind
terwijl de laatste wordt berekend als een scalair aggregaat, waarbij het stroomaggregaat vóór de samenvoeging alleen T2.col1
heeft invoert en heeft geen GROUP BY
eigenschap helemaal niet. Deze eenvoudigere opstelling heeft een meetbaar voordeel in termen van verminderde CPU-tijd.
Voor het geval van 10.000 rijen is er een extra verschil in het subqueryplan. Het voegt een gretige spoel
toe die alle ind,cast(col1 as bigint)
. kopieert waarden in tempdb
. In het geval dat snapshot-isolatie is ingeschakeld, is dit compacter dan de geclusterde indexstructuur en het netto-effect is om het aantal leesbewerkingen met ongeveer 25% te verminderen (omdat de basistabel behoorlijk wat lege ruimte vrijhoudt voor versie-informatie), wanneer deze optie is uitgeschakeld, werkt het minder compact (vermoedelijk vanwege de bigint
vs int
verschil) en meer leest resultaat. Dit verkleint de kloof tussen de subquery en groeperen op versies, maar de subquery wint nog steeds.
De duidelijke winnaar was echter de Recursive CTE. Voor de "no gaps"-versie zijn de logische reads van de basistabel nu 2 x (n + 1)
weerspiegelt de n
index zoekt in de 2-niveau-index om alle rijen op te halen plus de extra aan het einde die niets retourneert en de recursie beëindigt. Dat betekende echter nog steeds 20.002 reads om een tabel van 22 pagina's te verwerken!
Logische werktabellezingen voor de recursieve CTE-versie zijn erg hoog. Het lijkt te werken bij 6 werktabellezingen per bronrij. Deze komen van de indexspoel die de uitvoer van de vorige rij opslaat en vervolgens in de volgende iteratie opnieuw wordt gelezen (goede uitleg hiervan door Umachandar Jayachandran hier ). Ondanks het hoge aantal is dit nog steeds de best presterende.