Laufende Summe mit Zählung?


34

Wie der Titel schon sagt, brauche ich etwas Hilfe, um eine laufende Summe in T-SQL zu erhalten. Das Problem ist, dass die Summe, die ich machen muss, die Summe einer Zählung ist:

sum(count (distinct (customers))) 

Sagen Sie, wenn ich die Zählung alleine durchführe, wäre das Ergebnis:

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

Ich brauche die Ausgabe mit der Summe zu sein:

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

Ich habe vor der Verwendung der coalesceMethode Summen ausgeführt , aber nie mit einer Zählung. Ich bin nicht sicher, wie ich es jetzt machen soll, da ich die Zählung habe.


2
Welche Version von SQL Server bitte? Können Sie den Umfang der Daten teilen - sprechen wir von 1000 Zeilen, einer Million, einer Milliarde? Sind es wirklich nur diese beiden Spalten oder haben Sie das Schema für uns vereinfacht? Schließlich ist Dayein Schlüssel, und sind die Werte zusammenhängend?
Aaron Bertrand

Ich habe einen umfassenden Blog über die laufende Summe erstellt (Schrulliges Update vs. Hybrid Recursive CTE vs. Cursor): ienablemuch.com/2012/05/… Ich habe die laufende Summe, die einen reinen satzbasierten Ansatz verwendet, nicht berücksichtigt , die Leistung ist nichts zu sein Gewünscht: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen

Antworten:


53

Hier sind einige Methoden, die Sie vergleichen können. Lassen Sie uns zuerst eine Tabelle mit Dummy-Daten erstellen. Ich fülle dies mit einer Reihe zufälliger Daten aus sys.all_columns. Nun, es ist irgendwie zufällig - ich stelle sicher, dass die Daten zusammenhängend sind (was wirklich nur für eine der Antworten wichtig ist).

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;

Ergebnisse:

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

Die Daten sehen wie folgt aus (5000 Zeilen) - sehen jedoch auf Ihrem System je nach Version und Build-Nr .:

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
...

Die Ergebnisse der laufenden Summen sollten wie folgt aussehen (501 Zeilen):

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
...

Die Methoden, die ich vergleichen werde, sind:

  • "self-join" - der satzbasierte puristische Ansatz
  • "rekursiver CTE mit Datumsangaben" - dies basiert auf aufeinander folgenden Datumsangaben (keine Lücken)
  • "rekursiver CTE mit row_number" - ähnlich wie oben, jedoch langsamer, basierend auf ROW_NUMBER
  • "Rekursiver CTE mit #temp-Tabelle" - wie vorgeschlagen aus Mikaels Antwort gestohlen
  • "schrulliges Update", das, obwohl es nicht unterstützt wird und kein definiertes Verhalten verspricht, recht beliebt zu sein scheint
  • "Mauszeiger"
  • SQL Server 2012 mit neuer Fensterfunktion

selbst beitreten

Auf diese Weise werden Sie aufgefordert, dies zu tun, wenn Sie gewarnt werden, sich von Cursorn fernzuhalten, da "satzbasiert immer schneller ist". In einigen kürzlich durchgeführten Experimenten habe ich festgestellt, dass der Cursor diese Lösung übertrifft.

;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];

rekursives cte mit Daten

Erinnerung - Dies basiert auf zusammenhängenden Daten (keine Lücken), bis zu 10000 Rekursionsebenen und darauf, dass Sie das Startdatum des gewünschten Bereichs kennen (um den Anker zu setzen). Sie könnten den Anker natürlich mithilfe einer Unterabfrage dynamisch setzen, aber ich wollte die Dinge einfach halten.

;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);

rekursives cte mit row_number

Die Berechnung der Zeilennummer ist hier etwas teuer. Dies unterstützt wieder die maximale Rekursionsstufe von 10000, aber Sie müssen den Anker nicht zuweisen.

;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);

rekursives cte mit temporärer Tabelle

Wie vorgeschlagen, aus Mikaels Antwort stehlen, um dies in die Tests einzubeziehen.

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;

schrulliges Update

Wiederum beziehe ich dies nur der Vollständigkeit halber ein; Ich persönlich würde mich nicht auf diese Lösung verlassen, da, wie ich bereits in einer anderen Antwort erwähnt habe, diese Methode nicht garantiert funktioniert und möglicherweise in einer zukünftigen Version von SQL Server völlig kaputt geht. (Ich gebe mein Bestes, um SQL Server dazu zu zwingen, die von mir gewünschte Reihenfolge einzuhalten, wobei ich einen Hinweis für die Indexauswahl verwende.)

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;

Mauszeiger

"Vorsicht, hier gibt es Cursor! Cursor sind böse! Sie sollten Cursor um jeden Preis meiden!" Nein, das rede ich nicht, ich höre nur viel. Entgegen der landläufigen Meinung gibt es einige Fälle, in denen Cursor angebracht sind.

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

Wenn Sie mit der neuesten Version von SQL Server arbeiten, können wir durch Verbesserungen der Fensterfunktionen die laufenden Summen auf einfache Weise berechnen, ohne die exponentiellen Kosten für die Selbstverknüpfung (die Summe wird in einem Durchgang berechnet) und die Komplexität der CTEs (einschließlich der Anforderung) zusammenhängender Zeilen für eine bessere Leistung des CTE), das nicht unterstützte eigenartige Update und den verbotenen Cursor. Achten Sie nur auf den Unterschied zwischen der Verwendung von RANGEund ROWSoder darauf, dass Sie überhaupt nichts angeben - dies ROWSvermeidet nur eine Spool-Funktion auf der Festplatte, die ansonsten die Leistung erheblich beeinträchtigt.

;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];

Leistungsvergleiche

Ich verfolgte jeden Ansatz und wickelte ihn wie folgt in eine Charge ein:

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

Hier sind die Ergebnisse der Gesamtdauer in Millisekunden (denken Sie daran, dass dies jedes Mal auch die DBCC-Befehle einschließt):

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

Und ich habe es wieder ohne die DBCC-Befehle gemacht:

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

Entfernen Sie sowohl den DBCC als auch die Schleifen, indem Sie nur eine rohe Iteration messen:

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

Schließlich multiplizierte ich die Zeilenzahl in der Quelltabelle mit 10 (indem ich die Spitze auf 50000 änderte und eine weitere Tabelle als Cross-Join hinzufügte). Das Ergebnis ist eine einzige Iteration ohne DBCC-Befehle (einfach aus Zeitgründen):

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

Ich habe nur die Dauer gemessen - ich überlasse es dem Leser als Übung, diese Ansätze mit ihren Daten zu vergleichen und andere Metriken zu vergleichen, die wichtig sein können (oder mit ihrem Schema / ihren Daten variieren können). Bevor Sie aus dieser Antwort Schlussfolgerungen ziehen, müssen Sie sie anhand Ihrer Daten und Ihres Schemas testen. Diese Ergebnisse werden sich mit ziemlicher Sicherheit ändern, je höher die Zeilenanzahl ist.


Demo

Ich habe eine SQLFiddle hinzugefügt . Ergebnisse:

Bildbeschreibung hier eingeben


Fazit

In meinen Tests wäre die Wahl:

  1. SQL Server 2012-Methode, wenn SQL Server 2012 verfügbar ist.
  2. Wenn SQL Server 2012 nicht verfügbar ist und meine Daten zusammenhängend sind, würde ich die rekursive Methode cte with dates verwenden.
  3. Wenn weder 1. noch 2. zutreffen, würde ich mit dem Self-Join über das skurrile Update gehen, obwohl die Leistung knapp war, nur weil das Verhalten dokumentiert und garantiert ist. Ich mache mir weniger Sorgen um die zukünftige Kompatibilität, denn wenn das eigentümliche Update kaputt geht, ist es hoffentlich vorbei, nachdem ich meinen gesamten Code bereits auf 1 konvertiert habe. :-)

Aber auch hier sollten Sie diese anhand Ihres Schemas und Ihrer Daten testen. Da dies ein erfundener Test mit relativ geringen Reihenzahlen war, kann es auch ein Furz im Wind sein. Ich habe andere Tests mit unterschiedlichen Schemata und Zeilenzahlen durchgeführt und die Performance-Heuristiken waren sehr unterschiedlich. Deshalb habe ich so viele Folgefragen zu Ihrer ursprünglichen Frage gestellt.


AKTUALISIEREN

Ich habe hier mehr darüber gebloggt:

Beste Ansätze zum Ausführen von Summen - aktualisiert für SQL Server 2012


1

Dies ist anscheinend die optimale Lösung

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

Irgendwelche Ideen, ohne eine temporäre Tabelle zu implementieren (mein Prozess erzwingt bereits notwendigerweise Werte durch mehrere temporäre Tabellen, also versuche ich einen Weg zu finden, um die Verwendung einer anderen temporären Tabelle zu vermeiden)? Wenn nicht, werde ich diese Methode verwenden. Ich denke, es wird funktionieren

Dies kann auch mit einer Selbstverknüpfung oder einer verschachtelten Unterabfrage erfolgen, wobei diese Optionen bei weitem nicht so gut funktionieren. Außerdem ist es wahrscheinlich, dass Sie mit diesen Alternativen mit etwas Spooling oder Arbeitstabellen ohnehin Tempdb erreichen werden.

3
Beachten Sie jedoch, dass diese "kuriose Aktualisierungsmethode" möglicherweise nicht funktioniert. Diese Syntax wird nicht unterstützt und ihr Verhalten ist nicht definiert. Sie kann in einer zukünftigen Version, einem Hotfix oder einem Service Pack beschädigt werden. Das ist zwar schneller als einige unterstützte Alternativen, birgt aber potenzielle zukünftige Kompatibilitätskosten.
Aaron Bertrand

6
Es gibt viele Vorbehalte zu diesem Ansatz, die Jeff Moden irgendwo geschrieben hat. Sie sollten beispielsweise einen Clustered-Index haben day.
Martin Smith

2
@MartinSmith Es ist ein SEHR GROSSER Artikel bei sqlservercentral.com (gehe zur Autorenseite und finde seine Artikel über Quirck-Updates).
Fabricio Araujo

-2

Nur ein anderer Weg, teuer, aber versionsunabhängig. Es werden keine temporären Tabellen oder Variablen verwendet.

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
Das ist nicht gut, das ist sehr langsam. Selbst wenn Sie nur 100 Zeilen haben, wird zwischen den Tabellen 5.050 Mal ein Ping-Pong-Lesevorgang ausgeführt. 200 Zeilen, ist 20.100 Mal. Mit 1.000 Zeilen nur springt, es exponentiell zu 500.500 liest sqlblog.com/blogs/adam_machanic/archive/2006/07/12/...
Michael Buen

Nachdem ich das gepostet habe, habe ich den Link zu deinem Blog gesehen. Jetzt sehe ich, dass dies eine sehr schlechte Idee ist. Danke!
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.