За тези, които не използват SQL Server 2012 или по-нова версия, курсорът вероятно е най-ефективният поддържан и гарантирано метод извън CLR. Има и други подходи като „странната актуализация“, която може да бъде малко по-бърза, но не е гарантирана, че ще работи в бъдеще, и разбира се подходи, базирани на набори с хиперболични профили на производителност, когато таблицата става по-голяма, и рекурсивни CTE методи, които често изискват директни #tempdb I/O или водят до разливи, които дават приблизително същото въздействие.
INNER JOIN - не правете това:
Бавният, базиран на набори подход е от формата:
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;
Защо това е бавно? Тъй като таблицата става по-голяма, всеки нарастващ ред изисква четене на n-1 реда в таблицата. Това е експоненциално и е обвързано с неуспехи, изчакване или просто ядосани потребители.
Корелирана подзаявка - не правете и това:
Формулярът за подзаявка е също толкова болезнен по подобни болезнени причини.
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;
Странна актуализация - направете това на свой собствен риск:
Методът „странна актуализация“ е по-ефективен от горния, но поведението не е документирано, няма гаранции за реда и поведението може да работи днес, но може да се счупи в бъдеще. Включвам това, защото е популярен метод и е ефективен, но това не означава, че го подкрепям. Основната причина, поради която дори отговорих на този въпрос, вместо да го затварям като дубликат, е, че другият въпрос има странна актуализация като приет отговор.
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;
Рекурсивни CTEs
Този първи разчита на TID да бъде съседен, без празнини:
;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);
Ако не можете да разчитате на това, тогава можете да използвате този вариант, който просто изгражда непрекъсната последователност с помощта на 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);
В зависимост от размера на данните (напр. колони, за които не знаем), може да намерите по-добра цялостна производителност, като поставите съответните колони първо само в таблица #temp и обработвате спрямо нея вместо в основната таблица:
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;
Само първият CTE метод ще осигури производителност, съперничеща на странната актуализация, но прави голямо предположение за естеството на данните (без пропуски). Другите два метода ще се върнат назад и в тези случаи можете също да използвате курсор (ако не можете да използвате CLR и все още не сте на SQL Server 2012 или по-нова версия).
Курсор
На всички се казва, че курсорите са зло и че трябва да се избягват на всяка цена, но това всъщност бие производителността на повечето други поддържани методи и е по-безопасно от странната актуализация. Единствените, които предпочитам пред решението на курсора, са методите 2012 и CLR (по-долу):
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 или по-нова версия
Новите функции на прозореца, въведени в SQL Server 2012, правят тази задача много по-лесна (и тя се представя по-добре от всички горепосочени методи):
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
Обърнете внимание, че при по-големи набори от данни ще откриете, че горното се представя много по-добре от която и да е от следните две опции, тъй като RANGE използва пул на диск (а по подразбиране използва RANGE). Важно е също да се отбележи, че поведението и резултатите могат да се различават, така че се уверете, че и двамата връщат правилни резултати, преди да вземете решение между тях въз основа на тази разлика.
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
За пълнота предлагам връзка към метода CLR на Павел Павловски, който е най-предпочитаният метод за версии преди SQL Server 2012 (но не и 2000 очевидно).
http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/
Заключение
Ако използвате SQL Server 2012 или по-нова версия, изборът е очевиден - използвайте новия SUM() OVER()
конструкция (с ROWS
спрямо RANGE
). За по-ранни версии ще искате да сравните производителността на алтернативните подходи за вашата схема, данни и - като имате предвид фактори, които не са свързани с производителността - да определите кой подход е подходящ за вас. Много добре може да е подходът на CLR. Ето моите препоръки, по реда на предпочитание:
SUM() OVER() ... ROWS
, ако е от 2012 г. или по-нова- Метод CLR, ако е възможно
- Първият рекурсивен CTE метод, ако е възможно
- Курсор
- Другите рекурсивни CTE методи
- Странна актуализация
- Присъединяване и/или корелирана подзаявка
За допълнителна информация със сравнения на производителността на тези методи вижте този въпрос на http://dba.stackexchange.com:
https://dba.stackexchange.com/questions/19507/running-total-with-count
Освен това публикувах в блога повече подробности за тези сравнения тук:
http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals
Също така за групирани/разделени текущи суми вижте следните публикации:
http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals
Разделянето води до текуща заявка за общи суми
Множество текущи суми с групиране по