Suma bieżąca z liczbą?


34

Jak sugeruje tytuł, potrzebuję pomocy w uzyskaniu bieżącej sumy w języku T-SQL. Problem polega na tym, że suma, którą muszę zrobić, jest sumą liczby:

sum(count (distinct (customers))) 

Powiedz, że gdybym sam wyliczył liczbę, wynik byłby następujący:

Day | CountCustomers
----------------------
5/1  |      1
5/2  |      0
5/3  |      5

Potrzebuję danych wyjściowych z sumą, która będzie:

Day | RunningTotalCustomers
----------------------
5/1  |      1
5/2  |      1
5/3  |      6

Zrobiłem sumowanie przed użyciem tej coalescemetody, ale nigdy z liczeniem. Nie jestem pewien, jak to zrobić, skoro mam już rachubę.


2
Jaką wersję programu SQL Server proszę? Czy możesz podzielić zakres danych - czy mówimy o 1000 wierszy, milion, miliard? Czy to naprawdę tylko te dwie kolumny, czy może uprościłeś dla nas schemat? Wreszcie Dayklucz jest, a czy wartości są ciągłe?
Aaron Bertrand

Zrobiłem obszerny blog na temat sumy bieżącej (Dziwaczna aktualizacja vs Hybrid Recursive CTE vs Cursor): ienablemuch.com/2012/05/ ... Nie uwzględniłem sumy bieżącej, która wykorzystuje podejście oparte na czystym zestawie, wydajność nie jest niczym pożądane: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Odpowiedzi:


53

Oto kilka metod, które możesz porównać. Najpierw skonfigurujmy tabelę z danymi pozorowanymi. Wypełniam to losowymi danymi z sys.all_columns. Cóż, to trochę losowe - upewniam się, że daty są ciągłe (co jest tak naprawdę ważne tylko dla jednej z odpowiedzi).

CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);

CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);

INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
  COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
       s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;

SELECT 
  Earliest_Day   = MIN([Day]), 
  Latest_Day     = MAX([Day]), 
  Unique_Days    = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1, 
  Total_Rows     = COUNT(*)
FROM dbo.Hits;

Wyniki:

Earliest_Day         Latest_Day           Unique_Days  Total_Days
-------------------  -------------------  -----------  ----------
2012-05-01 00:00:00  2013-09-13 00:00:00  501          5000

Dane wyglądają tak (5000 wierszy) - ale będą wyglądać nieco inaczej w zależności od wersji i wersji #:

Day                  CustomerID
-------------------  ---
2012-05-01 00:00:00  95
2012-05-01 00:00:00  97
2012-05-01 00:00:00  97
2012-05-01 00:00:00  117
2012-05-01 00:00:00  100
...
2012-05-02 00:00:00  110
2012-05-02 00:00:00  110
2012-05-02 00:00:00  95
...

Wyniki sumy bieżącej powinny wyglądać tak (501 wierszy):

Day                  c   rt
-------------------  --  --
2012-05-01 00:00:00  6   6
2012-05-02 00:00:00  5   11
2012-05-03 00:00:00  4   15
2012-05-04 00:00:00  7   22
2012-05-05 00:00:00  6   28
...

Metody, które zamierzam porównać to:

  • „self-join” - purystyczne podejście oparte na zestawie
  • „rekurencyjne CTE z datami” - zależy to od ciągłych dat (bez przerw)
  • „rekurencyjne CTE z numerem_wiersza” - podobne do powyższego, ale wolniejsze, zależne od ROW_NUMBER
  • „rekurencyjne CTE ze stołem #temp” - skradzione z odpowiedzi Mikaela, jak sugerowano
  • „dziwaczna aktualizacja”, która choć nie jest obsługiwana i nie obiecuje określonego zachowania, wydaje się dość popularna
  • "kursor"
  • SQL Server 2012 przy użyciu nowej funkcji okienkowania

dołączyć do siebie

W ten sposób ludzie każą ci to robić, gdy ostrzegają, abyś trzymał się z dala od kursorów, ponieważ „oparte na zestawie jest zawsze szybsze”. W niektórych ostatnich eksperymentach odkryłem, że kursor wyprzedza to rozwiązanie.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
  FROM g INNER JOIN g AS g2
  ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];

rekurencyjne cte z datami

Przypomnienie - zależy to od ciągłych dat (bez przerw), do 10000 poziomów rekurencji oraz znajomości daty początkowej interesującego Cię zakresu (aby ustawić kotwicę). Oczywiście można dynamicznie ustawić kotwicę za pomocą podzapytania, ale chciałem, aby wszystko było proste.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], c, rt = c
        FROM g
        WHERE [Day] = '20120501'
    UNION ALL
    SELECT g.[Day], g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

rekurencyjne cte z numer_wiersza

Obliczenie numer_wiersza jest tutaj nieco drogie. Ponownie obsługuje to maksymalny poziom rekurencji wynoszący 10000, ale nie trzeba przypisywać kotwicy.

;WITH g AS 
(
  SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY), 
    c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], rn, c, rt = c
        FROM g
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

rekurencyjny cte z tabelą temp

Kradzież z odpowiedzi Mikaela, zgodnie z sugestią, aby uwzględnić to w testach.

CREATE TABLE #Hits
(
  rn INT PRIMARY KEY,
  c INT,
  [Day] SMALLDATETIME
);

INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
       COUNT(DISTINCT CustomerID),
       [Day]
FROM dbo.Hits
GROUP BY [Day];

WITH x AS
(
    SELECT [Day], rn, c, rt = c
        FROM #Hits as c
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN #Hits as g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

DROP TABLE #Hits;

dziwna aktualizacja

Ponownie włączam to tylko dla kompletności; Ja osobiście nie polegałbym na tym rozwiązaniu, ponieważ, jak wspomniałem w innej odpowiedzi, nie gwarantuje się, że ta metoda w ogóle zadziała i może całkowicie zepsuć się w przyszłej wersji SQL Server. (Dokładam wszelkich starań, aby zmusić program SQL Server do wykonania żądanej kolejności, korzystając ze wskazówek dotyczących wyboru indeksu).

CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);

INSERT #x([Day], c) 
    SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt1 INT;
SET @rt1 = 0;

UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);

SELECT [Day], c, rt FROM #x ORDER BY [Day];

DROP TABLE #x;

kursor

„Uwaga, są tu kursory! Kursory są złe! Powinieneś unikać kursorów za wszelką cenę!” Nie, to nie ja mówię, to tylko rzeczy, które dużo słyszę. Wbrew powszechnej opinii kursory są odpowiednie.

CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);

INSERT #x2([Day], c) 
    SELECT [Day], COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT [Day], c FROM #x2 ORDER BY [Day];

OPEN c;

FETCH NEXT FROM c INTO @d, @c;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt2 = @rt2 + @c;
  UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
  FETCH NEXT FROM c INTO @d, @c;
END

SELECT [Day], c, rt FROM #x2 ORDER BY [Day];

DROP TABLE #x2;

SQL Server 2012

Jeśli korzystasz z najnowszej wersji programu SQL Server, ulepszenia funkcji okienkowania pozwalają nam łatwo obliczyć sumy bieżące bez wykładniczego kosztu samozłączenia (suma obliczana jest w jednym przebiegu), złożoność CTE (w tym wymaganie ciągłych wierszy dla lepszej wydajności CTE), nieobsługiwana dziwaczna aktualizacja i zabroniony kursor. Wystarczy uważać na różnicę pomiędzy użyciem RANGEi ROWS, lub nie określając w ogóle - tylko ROWSunika się szpulę na dysku, która będzie utrudniać wydajność znacznie inaczej.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], c, 
  rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];

porównania wydajności

Podjąłem każde podejście i zapakowałem je w partię, używając:

SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();

Oto wyniki całkowitego czasu trwania w milisekundach (pamiętaj, że dotyczy to również poleceń DBCC za każdym razem):

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1296 ms   1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1655 ms   1516 ms
recursive cte with row_number   19747 ms  19630 ms
recursive cte with #temp table   1624 ms   1329 ms
quirky update                     880 ms   1030 ms -- non-SQL 2012 winner
cursor                           1962 ms   1850 ms
SQL Server 2012                   847 ms    917 ms -- winner if SQL 2012 available

Zrobiłem to ponownie bez poleceń DBCC:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1272 ms   1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1247 ms   1593 ms
recursive cte with row_number   18646 ms  18803 ms
recursive cte with #temp table   1340 ms   1564 ms
quirky update                    1024 ms   1116 ms -- non-SQL 2012 winner
cursor                           1969 ms   1835 ms
SQL Server 2012                   600 ms    569 ms -- winner if SQL 2012 available

Usuwając zarówno DBCC, jak i pętle, mierząc tylko jedną surową iterację:

method                          run 1     run 2
-----------------------------   --------  --------
self-join                         313 ms    242 ms
recursive cte with dates          217 ms    217 ms
recursive cte with row_number    2114 ms   1976 ms
recursive cte with #temp table     83 ms    116 ms -- "supported" non-SQL 2012 winner
quirky update                      86 ms     85 ms -- non-SQL 2012 winner
cursor                           1060 ms    983 ms
SQL Server 2012                    68 ms     40 ms -- winner if SQL 2012 available

Na koniec pomnożyłem liczbę wierszy w tabeli źródłowej przez 10 (zmieniając górę na 50000 i dodając kolejną tabelę jako połączenie krzyżowe). Rezultaty tego, jedna iteracja bez poleceń DBCC (po prostu w interesie czasu):

method                           run 1      run 2
-----------------------------    --------   --------
self-join                         2401 ms    2520 ms
recursive cte with dates           442 ms     473 ms
recursive cte with row_number   144548 ms  147716 ms
recursive cte with #temp table     245 ms     236 ms -- "supported" non-SQL 2012 winner
quirky update                      150 ms     148 ms -- non-SQL 2012 winner
cursor                            1453 ms    1395 ms
SQL Server 2012                    131 ms     133 ms -- winner

Zmierzyłem tylko czas trwania - pozostawię to jako ćwiczenie dla czytelnika, aby porównać te podejścia na ich danych, porównując inne metryki, które mogą być ważne (lub mogą różnić się w zależności od schematu / danych). Zanim wyciągniesz wnioski z tej odpowiedzi, od ciebie zależy sprawdzenie jej danych i schematu ... wyniki te prawie na pewno zmienią się, gdy liczba wierszy będzie wyższa.


próbny

Dodałem sqlfiddle . Wyniki:

wprowadź opis zdjęcia tutaj


wniosek

W moich testach wybór byłby następujący:

  1. Metoda SQL Server 2012, jeśli mam dostępny SQL Server 2012.
  2. Jeśli SQL Server 2012 nie jest dostępny, a moje daty są ciągłe, wybrałbym metodę rekurencyjną cte z datami.
  3. Jeśli ani 1., ani 2. nie mają zastosowania, wybrałbym samosprzężenie nad dziwaczną aktualizacją, nawet jeśli wydajność była bliska, tylko dlatego, że zachowanie jest udokumentowane i zagwarantowane. Mniej martwię się o przyszłą kompatybilność, ponieważ mam nadzieję, że jeśli dziwna aktualizacja się zepsuje, nastąpi to po przekonwertowaniu całego mojego kodu na 1. :-)

Ale ponownie powinieneś przetestować je pod kątem swojego schematu i danych. Ponieważ był to wymyślony test ze stosunkowo małą liczbą rzędów, równie dobrze może to być pierd na wietrze. Przeprowadziłem inne testy z różnymi schematami i liczbą wierszy, a heurystyka wydajności była całkiem inna ... dlatego zadałem tyle pytań uzupełniających do twojego pierwotnego pytania.


AKTUALIZACJA

Napisałem o tym więcej na blogu:

Najlepsze podejścia do uruchamiania sum - zaktualizowane do SQL Server 2012


1

Jest to najwyraźniej optymalne rozwiązanie

DECLARE @dailyCustomers TABLE (day smalldatetime, CountCustomers int, RunningTotal int)

DECLARE @RunningTotal int

SET @RunningTotal = 0

INSERT INTO @dailyCustomers 
SELECT day, CountCustomers, null
FROM Sales
ORDER BY day

UPDATE @dailyCustomers
SET @RunningTotal = RunningTotal = @RunningTotal + CountCustomers
FROM @dailyCustomers

SELECT * FROM @dailyCustomers

Wszelkie pomysły bez implementacji tabeli tymczasowej (mój proces już z konieczności wymusza wartości przez kilka tabel tymczasowych, więc próbuję znaleźć sposób na uniknięcie użycia innej tabeli tymczasowej)? Jeśli nie, użyję tej metody. Myślę, że to zadziała

Można to również zrobić z łączeniem własnym lub zagnieżdżonym podzapytaniem, ale te opcje nie działają tak dobrze. Jest również prawdopodobne, że i tak trafisz na tempdb za pomocą tych alternatyw z niektórymi buforami lub stołami roboczymi.

3
Pamiętaj tylko, że ta „dziwaczna aktualizacja” nie jest gwarantowana, że ​​działa - ta składnia nie jest obsługiwana, a jej zachowanie jest niezdefiniowane i może przerwać się w przyszłej wersji, poprawce lub dodatku service pack. Chociaż tak, jest szybszy niż niektóre obsługiwane alternatywy, ale wiąże się to z potencjalnym kosztem kompatybilności w przyszłości.
Aaron Bertrand

6
Istnieje wiele zastrzeżeń do tego podejścia, które Jeff Moden gdzieś napisał. Na przykład powinieneś mieć indeks klastrowany day.
Martin Smith,

2
@MartinSmith Jest to BARDZO WIELKI artykuł na sqlservercentral.com (przejdź do strony autora i znajdź jego „artykuły na temat dziwnych aktualizacji).
Fabricio Araujo,

-2

Po prostu inny sposób, kosztowny, ale niezależny od wersji. Nie używa tabel tymczasowych ani zmiennych.

select T.dday, T.CustomersByDay + 
    (select count(A.customer) from NewCustomersByDate A 
      where A.dday < T.dday) as TotalCustomerTillNow 
from (select dday, count(customer) as CustomersByDay 
        from NewCustomersByDate group by dday) T 

2
To nie jest dobre, to jest bardzo wolne. Nawet jeśli masz tylko 100 wierszy, wykona ping-pong odczytany między tabelami 5050 razy. 200 wierszy, jest 20 100 razy. Tylko 1000 wierszy przeskakuje wykładniczo do 500.500 odczytów sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Zobaczyłem link do twojego bloga po opublikowaniu tego, teraz widzę, że to bardzo zły pomysł, dzięki!
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.