Świetnym źródłem informacji o obliczaniu bieżących sum w SQL Server jest ten dokument
autorstwa Itzika Bena Gana, który został zgłoszony do zespołu SQL Server w ramach jego kampanii, aby uzyskać OVER
klauzula rozszerzona w stosunku do początkowej implementacji SQL Server 2005. W nim pokazuje, jak po dostaniu się do dziesiątek tysięcy wierszy kursory wykonują rozwiązania oparte na zbiorach. SQL Server 2012 rzeczywiście rozszerzył OVER
klauzula znacznie ułatwiająca tego rodzaju zapytania.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Ponieważ korzystasz z SQL Server 2005, nie jest to dla Ciebie dostępne.
Adam Machanic pokazuje tutaj jak CLR może być używany do poprawy wydajności standardowych kursorów TSQL.
Dla tej definicji tabeli
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Tworzę tabele z 2000 i 10 000 wierszy w bazie danych z ALLOW_SNAPSHOT_ISOLATION ON
i jeden z tym ustawieniem (powodem tego jest to, że moje początkowe wyniki były w bazie danych z włączonym ustawieniem, które prowadziło do zagadkowego aspektu wyników).
Indeksy klastrowane dla wszystkich tabel miały tylko 1 stronę główną. Liczba stron liści dla każdej z nich jest pokazana poniżej.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Przetestowałem następujące przypadki (Linki pokazują plany wykonania)
- Dołącz od lewej i grupuj według
- Skorelowane podzapytanie plan 2000 wierszy ,plan 10000 wierszy
- CTE z odpowiedzi Mikaela (zaktualizowanej)
- CTE poniżej
Powodem włączenia dodatkowej opcji CTE było zapewnienie rozwiązania CTE, które nadal będzie działać, jeśli ind
kolumna nie była gwarantowana sekwencyjnie.
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);
Wszystkie zapytania miały CAST(col1 AS BIGINT)
dodano w celu uniknięcia błędów przepełnienia w czasie wykonywania. Dodatkowo dla wszystkich przypisałem wyniki do zmiennych jak powyżej, aby wyeliminować czas poświęcony na odesłanie wyników z rozpatrzenia.
Wyniki
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | 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 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Zarówno skorelowane podzapytanie, jak i GROUP BY
wersja używa "trójkątnych" sprzężeń zagnieżdżonych sterowanych przez skanowanie indeksu klastrowego w RunningTotals
tabela (T1
) i dla każdego wiersza zwróconego przez ten skan, szukanie z powrotem do tabeli (T2
) samołączenie na T2.ind<=T1.ind
.
Oznacza to, że te same wiersze są przetwarzane wielokrotnie. Gdy T1.ind=1000
wiersz jest przetwarzany, samo łączenie pobiera i sumuje wszystkie wiersze z ind <= 1000
, a następnie dla następnego wiersza, gdzie T1.ind=1001
te same 1000 wierszy jest pobieranych ponownie i zsumowane wraz z jednym dodatkowym wierszem i tak dalej.
Łączna liczba takich operacji dla tabeli zawierającej 2000 wierszy wynosi 2 001 000, dla 10 tys. wierszy 50 005 000 lub ogólniej (n² + n) / 2
który wyraźnie rośnie wykładniczo.
W przypadku 2000 wierszy główna różnica między GROUP BY
a wersje podzapytania polegają na tym, że pierwsza z nich ma agregację strumienia po łączeniu, a więc ma trzy kolumny zasilające do niej (T1.ind
, T2.col1
, T2.col1
) i GROUP BY
właściwość T1.ind
podczas gdy ta ostatnia jest obliczana jako agregacja skalarna, z agregacją strumienia przed sprzężeniem, ma tylko T2.col1
karmienie do niego i nie ma GROUP BY
zestaw właściwości. Ten prostszy układ daje wymierne korzyści w postaci skrócenia czasu procesora.
W przypadku 10 000 wierszy istnieje dodatkowa różnica w planie podrzędnych zapytań. Dodaje gorącą szpulę
który kopiuje wszystkie ind,cast(col1 as bigint)
wartości do tempdb
. W przypadku, gdy izolacja migawek jest włączona, działa to bardziej kompaktowo niż struktura indeksu klastrowego, a efektem netto jest zmniejszenie liczby odczytów o około 25% (ponieważ tabela bazowa zachowuje dość dużo pustego miejsca na informacje o wersji), gdy ta opcja jest wyłączona, działa mniej kompaktowo (prawdopodobnie ze względu na bigint
vs int
różnica) i więcej odczytów wynik. Zmniejsza to lukę między zapytaniem podrzędnym a grupowaniem według wersji, ale zapytanie podrzędne nadal wygrywa.
Wyraźnym zwycięzcą był jednak rekursywny CTE. Dla wersji „bez przerw” odczyty logiczne z tabeli bazowej wynoszą teraz 2 x (n + 1)
odzwierciedla n
index szuka w indeksie 2 poziomu, aby pobrać wszystkie wiersze plus dodatkowy na końcu, który nic nie zwraca i kończy rekurencję. To nadal oznaczało 20,002 odczytów, aby przetworzyć 22-stronicową tabelę!
Logiczne odczyty tabeli roboczej dla wersji rekurencyjnej CTE są bardzo wysokie. Wydaje się, że działa przy 6 odczytach z tabeli roboczej na wiersz źródłowy. Pochodzą one ze szpuli indeksu, która przechowuje dane wyjściowe poprzedniego wiersza, a następnie jest odczytywana ponownie w następnej iteracji (dobre wyjaśnienie tego przez Umachandara Jayachandrana tutaj ). Pomimo dużej liczby jest to nadal najlepszy produkt.