For dem, der ikke bruger SQL Server 2012 eller nyere, er en markør sandsynligvis den mest effektive understøttede og garanteret metode uden for CLR. Der er andre tilgange såsom den "quirky opdatering", som kan være marginalt hurtigere, men ikke garanteret vil virke i fremtiden, og selvfølgelig sæt-baserede tilgange med hyperbolske præstationsprofiler, efterhånden som tabellen bliver større, og rekursive CTE-metoder, der ofte kræver direkte #tempdb I/O eller resultere i spild, der giver nogenlunde samme effekt.
INNER JOIN - gør ikke dette:
Den langsomme, sæt-baserede tilgang har formen:
SELECT t1.TID, t1.amt, RunningTotal = SUM(t2.amt)
FROM dbo.Transactions AS t1
INNER JOIN dbo.Transactions AS t2
ON t1.TID >= t2.TID
GROUP BY t1.TID, t1.amt
ORDER BY t1.TID;
Grunden til at dette er langsomt? Efterhånden som tabellen bliver større, kræver hver trinvis række læsning af n-1 rækker i tabellen. Dette er eksponentielt og bundet til fejl, timeouts eller bare vrede brugere.
Korreleret underforespørgsel - gør heller ikke dette:
Underforespørgselsformularen er på samme måde smertefuld af lignende smertefulde årsager.
SELECT TID, amt, RunningTotal = amt + COALESCE(
(
SELECT SUM(amt)
FROM dbo.Transactions AS i
WHERE i.TID < o.TID), 0
)
FROM dbo.Transactions AS o
ORDER BY TID;
Quirky opdatering - gør dette på egen risiko:
Den "quirky update" metode er mere effektiv end ovenstående, men adfærden er ikke dokumenteret, der er ingen garantier for orden, og adfærden fungerer muligvis i dag, men kan gå i stykker i fremtiden. Jeg medtager dette, fordi det er en populær metode, og det er effektivt, men det betyder ikke, at jeg støtter det. Den primære grund til, at jeg selv besvarede dette spørgsmål i stedet for at lukke det som en dublet, er, fordi det andet spørgsmål har en finurlig opdatering som det accepterede svar.
DECLARE @t TABLE
(
TID INT PRIMARY KEY,
amt INT,
RunningTotal INT
);
DECLARE @RunningTotal INT = 0;
INSERT @t(TID, amt, RunningTotal)
SELECT TID, amt, RunningTotal = 0
FROM dbo.Transactions
ORDER BY TID;
UPDATE @t
SET @RunningTotal = RunningTotal = @RunningTotal + amt
FROM @t;
SELECT TID, amt, RunningTotal
FROM @t
ORDER BY TID;
Rekursive CTE'er
Denne første er afhængig af, at TID er sammenhængende, ingen huller:
;WITH x AS
(
SELECT TID, amt, RunningTotal = amt
FROM dbo.Transactions
WHERE TID = 1
UNION ALL
SELECT y.TID, y.amt, x.RunningTotal + y.amt
FROM x
INNER JOIN dbo.Transactions AS y
ON y.TID = x.TID + 1
)
SELECT TID, amt, RunningTotal
FROM x
ORDER BY TID
OPTION (MAXRECURSION 10000);
Hvis du ikke kan stole på dette, kan du bruge denne variant, som blot bygger en sammenhængende sekvens ved hjælp af ROW_NUMBER()
:
;WITH y AS
(
SELECT TID, amt, rn = ROW_NUMBER() OVER (ORDER BY TID)
FROM dbo.Transactions
), x AS
(
SELECT TID, rn, amt, rt = amt
FROM y
WHERE rn = 1
UNION ALL
SELECT y.TID, y.rn, y.amt, x.rt + y.amt
FROM x INNER JOIN y
ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
FROM x
ORDER BY x.rn
OPTION (MAXRECURSION 10000);
Afhængigt af størrelsen af dataene (f.eks. kolonner, vi ikke kender til), kan du muligvis finde bedre generel ydeevne ved kun at fylde de relevante kolonner i en #temp-tabel først og behandle mod den i stedet for basistabellen:
CREATE TABLE #x
(
rn INT PRIMARY KEY,
TID INT,
amt INT
);
INSERT INTO #x (rn, TID, amt)
SELECT ROW_NUMBER() OVER (ORDER BY TID),
TID, amt
FROM dbo.Transactions;
;WITH x AS
(
SELECT TID, rn, amt, rt = amt
FROM #x
WHERE rn = 1
UNION ALL
SELECT y.TID, y.rn, y.amt, x.rt + y.amt
FROM x INNER JOIN #x AS y
ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
FROM x
ORDER BY TID
OPTION (MAXRECURSION 10000);
DROP TABLE #x;
Kun den første CTE-metode vil levere ydeevne, der konkurrerer med den finurlige opdatering, men den gør en stor antagelse om dataenes art (ingen huller). De to andre metoder vil falde tilbage, og i de tilfælde kan du lige så godt bruge en markør (hvis du ikke kan bruge CLR, og du endnu ikke er på SQL Server 2012 eller nyere).
Markør
Alle får at vide, at markører er onde, og at de bør undgås for enhver pris, men dette slår faktisk ydeevnen for de fleste andre understøttede metoder og er sikrere end den skæve opdatering. De eneste, jeg foretrækker frem for markørløsningen, er 2012- og CLR-metoderne (nedenfor):
CREATE TABLE #x
(
TID INT PRIMARY KEY,
amt INT,
rt INT
);
INSERT #x(TID, amt)
SELECT TID, amt
FROM dbo.Transactions
ORDER BY TID;
DECLARE @rt INT, @tid INT, @amt INT;
SET @rt = 0;
DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR SELECT TID, amt FROM #x ORDER BY TID;
OPEN c;
FETCH c INTO @tid, @amt;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @rt = @rt + @amt;
UPDATE #x SET rt = @rt WHERE TID = @tid;
FETCH c INTO @tid, @amt;
END
CLOSE c; DEALLOCATE c;
SELECT TID, amt, RunningTotal = rt
FROM #x
ORDER BY TID;
DROP TABLE #x;
SQL Server 2012 eller nyere
Nye vinduesfunktioner introduceret i SQL Server 2012 gør denne opgave meget nemmere (og den yder også bedre end alle ovenstående metoder):
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
Bemærk, at på større datasæt vil du opdage, at ovenstående yder meget bedre end nogen af de følgende to muligheder, da RANGE bruger en spool på disken (og standarden bruger RANGE). Det er dog også vigtigt at bemærke, at adfærd og resultater kan variere, så sørg for, at de begge returnerer korrekte resultater, før du beslutter mellem dem baseret på denne forskel.
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID)
FROM dbo.Transactions
ORDER BY TID;
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID RANGE UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
CLR
For fuldstændighedens skyld tilbyder jeg et link til Pavel Pawlowskis CLR-metode, som er langt den foretrukne metode på versioner før SQL Server 2012 (men naturligvis ikke 2000).
http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/
Konklusion
Hvis du er på SQL Server 2012 eller nyere, er valget oplagt - brug den nye SUM() OVER()
konstruktion (med ROWS
vs. RANGE
). For tidligere versioner vil du gerne sammenligne ydeevnen af de alternative tilgange på dit skema, data og - tage ikke-ydelsesrelaterede faktorer i tankerne - bestemme, hvilken tilgang der er den rigtige for dig. Det kan meget vel være CLR-tilgangen. Her er mine anbefalinger i foretrukket rækkefølge:
SUM() OVER() ... ROWS
, hvis den er i 2012 eller derover- CLR-metode, hvis det er muligt
- Første rekursive CTE-metode, hvis det er muligt
- Markør
- De andre rekursive CTE-metoder
- Quirky opdatering
- Deltag og/eller korreleret underforespørgsel
For yderligere information om præstationssammenligninger af disse metoder, se dette spørgsmål på http://dba.stackexchange.com:
https://dba.stackexchange.com/questions/19507/running-total-with-count
Jeg har også blogget flere detaljer om disse sammenligninger her:
http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals
Også for grupperede/partitionerede løbende totaler, se følgende indlæg:
http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals
Partitionering resulterer i en kørende totalforespørgsel
Flere løbende totaler med Grupper efter