Страхотен ресурс за изчисляване на текущи суми в SQL Server е този документ
от Ицик Бен Ган, който беше изпратен на екипа на SQL Server като част от кампанията му за OVER
клауза, разширена допълнително от първоначалната си реализация на SQL Server 2005. В него той показва как след като влезете в десетки хиляди редове, курсорите навън изпълняват базирани на набор решения. SQL Server 2012 наистина разшири OVER
клауза, която прави този вид заявка много по-лесна.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Тъй като сте на SQL Server 2005 обаче, това не е достъпно за вас.
Адам Маханик се показва тук как CLR може да се използва за подобряване на производителността на стандартните TSQL курсори.
За тази дефиниция на таблица
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Създавам таблици с 2000 и 10 000 реда в база данни с ALLOW_SNAPSHOT_ISOLATION ON
и един с изключена тази настройка (Причината за това е, че моите първоначални резултати бяха в DB с включена настройка, което доведе до озадачаващ аспект на резултатите).
Клъстерираните индекси за всички таблици имаха само 1 основна страница. Броят листни страници за всеки е показан по-долу.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Тествах следните случаи (Връзките показват планове за изпълнение)
- Наляво присъединяване и групиране по
- Свързана подзаявка план с 2000 реда ,план с 10 000 реда
- CTE от (актуализирания) отговор на Микаел
- CTE по-долу
Причината за включването на допълнителната опция за CTE беше да се осигури решение за CTE, което все още ще работи, ако ind
колона не е гарантирано последователна.
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);
Всички заявки имаха CAST(col1 AS BIGINT)
добавен, за да се избегнат грешки при препълване по време на изпълнение. Освен това за всички тях присвоих резултатите на променливи, както по-горе, за да премахна времето, прекарано в изпращане на резултатите от разглеждане.
Резултати
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | 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 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Както корелираната подзаявка, така и GROUP BY
версията използва "триъгълни" вложени циклични присъединявания, управлявани от сканиране на клъстерен индекс на RunningTotals
таблица (T1
) и за всеки ред, върнат от това сканиране, търсене обратно в таблицата (T2
) самостоятелно присъединяване към T2.ind<=T1.ind
.
Това означава, че едни и същи редове се обработват многократно. Когато T1.ind=1000
редът се обработва, самосъединяването извлича и сумира всички редове с ind <= 1000
, след това за следващия ред, където T1.ind=1001
същите 1000 реда се извличат отново и сумирани заедно с един допълнителен ред и т.н.
Общият брой на такива операции за таблица с 2000 реда е 2 001 000, за 10k реда 50 005 000 или по-общо (n² + n) / 2
което очевидно расте експоненциално.
В случая с 2000 реда основната разлика между GROUP BY
и версиите на подзаявката е, че първата има агрегата на потока след присъединяването и така има три колони, подаващи се в него (T1.ind
, T2.col1
, T2.col1
) и GROUP BY
свойство на T1.ind
като има предвид, че последният се изчислява като скаларен агрегат, като агрегатът на потока преди присъединяването има само T2.col1
захранване в него и няма GROUP BY
имот набор изобщо. Може да се види, че тази по-проста подредба има измерима полза по отношение на намаленото процесорно време.
За случая с 10 000 реда има допълнителна разлика в плана на подзаявката. Той добавя нетърпелив спул
който копира всички ind,cast(col1 as bigint)
стойности в tempdb
. В случай, че изолацията на моментната снимка е включена, това работи по-компактно от структурата на клъстерирания индекс и нетният ефект е да се намали броят на четенията с около 25% (тъй като базовата таблица запазва доста празно място за информация за версии), когато тази опция е изключена, работи по-малко компактно (вероятно поради bigint
срещу int
разлика) и резултат от повече четения. Това намалява разликата между подзаявката и групирането по версии, но подзаявката все още печели.
Явният победител обаче беше рекурсивният CTE. За версията „без пропуски“ логическите четения от базовата таблица вече са 2 x (n + 1)
отразяващ n
index търси в индекса на 2 нива, за да извлече всички редове плюс допълнителния в края, който не връща нищо и прекратява рекурсията. Това все пак означаваше 20 002 четения за обработка на таблица от 22 страници!
Четенията на логическата работна таблица за рекурсивната CTE версия са много високи. Изглежда, че работи при 6 четения на работна маса на ред на източника. Те идват от макарата на индекса, която съхранява изхода от предишния ред, след което се чете отново при следващата итерация (добро обяснение за това от Umachandar Jayachandran тук ). Въпреки високия брой, това все още е най-доброто представяне.