En fantastisk ressource til beregning af løbende totaler i SQL Server er dette dokument
af Itzik Ben Gan, der blev sendt til SQL Server-teamet som en del af hans kampagne for at få OVER
klausul udvidet længere fra dens oprindelige SQL Server 2005-implementering. I den viser han, hvordan når du først kommer ind i titusindvis af rækker, udfører markørerne sæt-baserede løsninger. SQL Server 2012 udvidede faktisk OVER
klausul gør denne form for forespørgsel meget lettere.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Da du er på SQL Server 2005, er dette dog ikke tilgængeligt for dig.
Adam Machanic viser her hvordan CLR kan bruges til at forbedre ydeevnen af standard TSQL-markører.
For denne tabeldefinition
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Jeg opretter tabeller med både 2.000 og 10.000 rækker i en database med ALLOW_SNAPSHOT_ISOLATION TIL
og en med denne indstilling fra (Grunden til dette er, fordi mine første resultater var i en DB med indstillingen på, der førte til et forvirrende aspekt af resultaterne).
De klyngede indekser for alle tabeller havde kun 1 rodside. Antallet af bladsider for hver er vist nedenfor.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Jeg testede følgende cases (Links viser udførelsesplaner)
- Tilføj til venstre og grupper efter
- Korreleret underforespørgsel 2000 rækkeplan ,10000 rækkeplan
- CTE fra Mikaels (opdaterede) svar
- CTE nedenfor
Årsagen til at inkludere den ekstra CTE-mulighed var at levere en CTE-løsning, der stadig ville fungere, hvis ind
kolonne var ikke garanteret sekventiel.
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 forespørgslerne havde en CAST(col1 AS BIGINT)
tilføjet for at undgå overløbsfejl under kørsel. For dem alle tildelte jeg desuden resultaterne til variabler som ovenfor for at eliminere tid brugt på at sende resultater tilbage fra overvejelse.
Resultater
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | 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 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Både den korrelerede underforespørgsel og GROUP BY
version bruger "triangulære" indlejrede løkkesammenføjninger drevet af en klynget indeksscanning på RunningTotals
tabel (T1
) og for hver række, der returneres af den scanning, søges tilbage i tabellen (T2
) selvtilmelding på T2.ind<=T1.ind
.
Det betyder, at de samme rækker bliver behandlet gentagne gange. Når T1.ind=1000
rækken er behandlet, selvforeningen henter og summerer alle rækker med en ind <=1000
, derefter til næste række hvor T1.ind=1001
de samme 1000 rækker hentes igen og summeret sammen med en ekstra række og så videre.
Det samlede antal af sådanne operationer for en tabel med 2.000 rækker er 2.001.000, for 10.000 rækker 50.005.000 eller mere generelt (n² + n) / 2 som tydeligvis vokser eksponentielt.
I tilfældet med 2.000 rækker er den største forskel mellem GROUP BY
og underforespørgselsversionerne er, at førstnævnte har stream-aggregatet efter joinforbindelsen, og så har tre kolonner, der føres ind i det (T1.ind
, T2.col1
, T2.col1
) og en GROUP BY
egenskaben for T1.ind
hvorimod sidstnævnte beregnes som et skalært aggregat, hvor strømaggregatet før sammenføjningen kun har T2.col1
feeds ind i det og har ingen GROUP BY
ejendomssæt overhovedet. Dette enklere arrangement kan ses at have en målbar fordel i form af reduceret CPU-tid.
For sagen med 10.000 rækker er der en yderligere forskel i underforespørgselsplanen. Den tilføjer en ivrig spole
som kopierer alle ind,cast(col1 som bigint)
værdier i tempdb
. I det tilfælde, hvor snapshot-isolering er på, virker dette mere kompakt end den klyngede indeksstruktur, og nettoeffekten er at reducere antallet af læsninger med omkring 25 % (da basistabellen bevarer en hel del tom plads til versionsinformation), når denne mulighed er slået fra, virker den mindre kompakt (formodentlig på grund af bigint
). vs int
forskel) og resultatet af flere læsninger. Dette reducerer kløften mellem underforespørgslen og gruppen efter version, men underforespørgslen vinder stadig.
Den klare vinder var dog Recursive CTE. For versionen "ingen mellemrum" er logiske læsninger fra basistabellen nu 2 x (n + 1)
afspejler n
indeks søger ind i indekset på 2 niveauer for at hente alle rækkerne plus den ekstra i slutningen, der ikke returnerer noget og afslutter rekursionen. Det betød dog stadig 20.002 læsninger for at behandle en 22 siders tabel!
Logiske arbejdstabellæsninger for den rekursive CTE-version er meget høje. Det ser ud til at fungere ved 6 arbejdsbordlæsninger pr. kilderække. Disse kommer fra indeksspolen, der gemmer output fra den foregående række, og derefter læses fra igen i næste iteration (god forklaring af dette af Umachandar Jayachandran her ). På trods af det høje antal er dette stadig den bedste performer.