Dla tych, którzy nie używają SQL Server 2012 lub nowszego, kursor jest prawdopodobnie najbardziej wydajnym obsługiwanym i gwarantowane metoda poza CLR. Istnieją inne podejścia, takie jak „dziwaczna aktualizacja”, która może być nieznacznie szybsza, ale nie ma gwarancji, że zadziała w przyszłości, i oczywiście podejścia oparte na zbiorach z hiperbolicznymi profilami wydajności w miarę powiększania się tabeli oraz rekurencyjne metody CTE, które często wymagają bezpośredniego #tempdb I/O lub powodują wycieki, które mają mniej więcej taki sam wpływ.
DOŁĄCZENIE WEWNĘTRZNE — nie rób tego:
Powolne, oparte na zbiorach podejście ma postać:
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;
Dlaczego to jest powolne? W miarę powiększania się tabeli każdy kolejny wiersz wymaga odczytania n-1 wierszy w tabeli. Jest to wykładniczy i związany z awariami, przekroczeniami limitu czasu lub po prostu rozgniewanymi użytkownikami.
Skorelowane podzapytanie – też tego nie rób:
Forma podzapytania jest podobnie bolesna z podobnie bolesnych powodów.
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;
Dziwaczna aktualizacja – rób to na własne ryzyko:
Metoda „dziwacznej aktualizacji” jest bardziej wydajna niż powyższa, ale zachowanie nie jest udokumentowane, nie ma gwarancji co do porządku, a zachowanie może działać dzisiaj, ale może ulec awarii w przyszłości. Włączam to, ponieważ jest to popularna metoda i jest skuteczna, ale to nie znaczy, że ją popieram. Głównym powodem, dla którego odpowiedziałem na to pytanie, zamiast zamykać je jako duplikat, jest to, że drugie pytanie ma dziwaczną aktualizację jako zaakceptowaną odpowiedź.
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;
Rekursywne CTE
Ten pierwszy opiera się na TID, aby był ciągły, bez przerw:
;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);
Jeśli nie możesz na tym polegać, możesz użyć tej odmiany, która po prostu tworzy ciągłą sekwencję za pomocą 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);
W zależności od rozmiaru danych (np. kolumn, o których nie wiemy), możesz uzyskać lepszą ogólną wydajność, umieszczając najpierw odpowiednie kolumny tylko w tabeli #temp i przetwarzając je na podstawie tego zamiast tabeli podstawowej:
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;
Tylko pierwsza metoda CTE zapewni wydajność rywalizującą z dziwaczną aktualizacją, ale zakłada duże założenie co do natury danych (bez luk). Pozostałe dwie metody wycofają się iw takich przypadkach równie dobrze możesz użyć kursora (jeśli nie możesz używać CLR i nie korzystasz jeszcze z SQL Server 2012 lub nowszego).
Kursor
Wszystkim mówi się, że kursory są złe i należy ich unikać za wszelką cenę, ale to faktycznie bije wydajność większości innych obsługiwanych metod i jest bezpieczniejsze niż dziwaczna aktualizacja. Jedyne, które wolę od rozwiązania kursora, to metody 2012 i CLR (poniżej):
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 lub nowszy
Nowe funkcje okien wprowadzone w SQL Server 2012 znacznie ułatwiają to zadanie (i działają lepiej niż wszystkie powyższe metody):
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
Zauważ, że w przypadku większych zestawów danych powyższe działa znacznie lepiej niż którakolwiek z poniższych dwóch opcji, ponieważ RANGE używa buforowania na dysku (a domyślnie używa RANGE). Jednak ważne jest również, aby pamiętać, że zachowanie i wyniki mogą się różnić, więc upewnij się, że oba zwracają prawidłowe wyniki, zanim zdecydujesz między nimi na podstawie tej różnicy.
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
Dla kompletności oferuję link do metody CLR Pawła Pawłowskiego, która jest zdecydowanie preferowaną metodą w wersjach wcześniejszych niż SQL Server 2012 (ale oczywiście nie 2000).
http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/
Wniosek
Jeśli korzystasz z SQL Server 2012 lub nowszego, wybór jest oczywisty — użyj nowej metody SUM() OVER()
konstrukcja (z ROWS
w porównaniu z RANGE
). W przypadku wcześniejszych wersji warto porównać wydajność alternatywnych podejść w schemacie, danych i — biorąc pod uwagę czynniki niezwiązane z wydajnością — określić, które podejście jest dla Ciebie odpowiednie. Bardzo dobrze może to być podejście CLR. Oto moje zalecenia w kolejności preferencji:
SUM() OVER() ... ROWS
, jeśli w 2012 r. lub nowszym- Metoda CLR, jeśli to możliwe
- Pierwsza rekurencyjna metoda CTE, jeśli to możliwe
- Kursor
- Inne rekurencyjne metody CTE
- Dziwaczna aktualizacja
- Dołącz i/lub skorelowane podzapytanie
Więcej informacji na temat porównań wydajności tych metod można znaleźć w tym pytaniu na stronie http://dba.stackexchange.com:
https://dba.stackexchange.com/questions/19507/running-total-with-count
Więcej szczegółów na temat tych porównań napisałem tutaj:
http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals
Również w przypadku zgrupowanych/partycjonowanych sum bieżących zobacz następujące posty:
http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals
Partycjonowanie wyników w uruchomionym zapytaniu podsumowującym
Wiele sum bieżących z grupowaniem według