Totale parziale con conteggio?


34

Come suggerisce il titolo, ho bisogno di aiuto per ottenere un totale parziale in T-SQL. Il problema è che la somma che devo fare è la somma di un conteggio:

sum(count (distinct (customers))) 

Dire se ho eseguito il conteggio da solo, il risultato sarebbe:

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

Ho bisogno che l'output con la somma sia:

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

Ho eseguito i totali in esecuzione prima di utilizzare il coalescemetodo, ma mai con un conteggio. Non sono sicuro di come farlo ora che ho il conteggio.


2
Quale versione di SQL Server, per favore? Puoi condividere l'ambito dei dati? Stiamo parlando di circa 1000 righe, un milione, un miliardo? Sono davvero solo queste due colonne o hai semplificato lo schema per noi? Infine, è Dayuna chiave e i valori sono contigui?
Aaron Bertrand

Ho realizzato un blog completo sul totale parziale (aggiornamento eccentrico vs CTE ricorsivo ibrido vs cursore): ienablemuch.com/2012/05/… Non ho incluso il totale parziale che utilizza un approccio basato su set puro, le prestazioni non sono nulla da desiderato: sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen,

Risposte:


53

Ecco alcuni metodi che puoi confrontare. Innanzitutto impostiamo una tabella con alcuni dati fittizi. Sto compilando questo con un mucchio di dati casuali da sys.all_columns. Bene, è un po 'casuale - sto assicurando che le date siano contigue (il che è davvero importante solo per una delle risposte).

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;

risultati:

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

I dati sono simili a questo (5000 righe), ma appariranno leggermente diversi sul sistema in base alla versione e al numero di build:

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

E i risultati dei totali in esecuzione dovrebbero apparire così (501 righe):

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

Quindi i metodi che ho intenzione di confrontare sono:

  • "self-join" - l'approccio purista basato sul set
  • "CTE ricorsivo con date" - questo si basa su date contigue (senza lacune)
  • "CTE ricorsivo con row_number" - simile al precedente ma più lento, basandosi su ROW_NUMBER
  • "CTE ricorsivo con tabella #temp" - rubato dalla risposta di Mikael come suggerito
  • "aggiornamento stravagante" che, sebbene non definito e non promettente comportamento definito, sembra essere abbastanza popolare
  • "cursore"
  • SQL Server 2012 utilizzando la nuova funzionalità di finestre

self-join

Questo è il modo in cui le persone ti diranno di farlo quando ti avvertono di stare lontano dai cursori, perché "il set-based è sempre più veloce". In alcuni recenti esperimenti ho scoperto che il cursore supera questa soluzione.

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

cte ricorsivo con date

Promemoria: si basa su date contigue (senza spazi vuoti), fino a 10000 livelli di ricorsione e che conosci la data di inizio dell'intervallo che ti interessa (per impostare l'ancoraggio). Ovviamente potresti impostare l'ancora in modo dinamico usando una subquery, ma volevo mantenere le cose semplici.

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

cte ricorsivo con row_number

Il calcolo del numero di riga è leggermente costoso qui. Anche in questo caso supporta un livello massimo di ricorsione di 10000, ma non è necessario assegnare l'ancoraggio.

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

CTE ricorsivo con tabella temporanea

Rubare dalla risposta di Mikael, come suggerito, per includerlo nei test.

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;

aggiornamento eccentrico

Ancora una volta lo sto includendo solo per completezza; Personalmente non farei affidamento su questa soluzione poiché, come ho già detto in un'altra risposta, questo metodo non è garantito per funzionare e potrebbe interrompere completamente una versione futura di SQL Server. (Sto facendo del mio meglio per costringere SQL Server a obbedire all'ordine che voglio, usando un suggerimento per la scelta dell'indice.)

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;

cursore

"Attenzione, ci sono cursori qui! I cursori sono cattivi! Dovresti evitare i cursori a tutti i costi!" No, non sono io a parlare, sono solo cose che sento molto. Contrariamente all'opinione popolare, ci sono alcuni casi in cui i cursori sono appropriati.

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

Se si utilizza la versione più recente di SQL Server, i miglioramenti apportati alla funzionalità di finestre ci consentono di calcolare facilmente i totali in esecuzione senza il costo esponenziale del self-join (il SUM viene calcolato in un passaggio), la complessità dei CTE (incluso il requisito di righe contigue per il CTE con le migliori prestazioni), l'aggiornamento stravagante non supportato e il cursore proibito. Diffidare della differenza tra l'utilizzo RANGEe ROWS, o non la specifica, ROWSevita solo una spool su disco, che altrimenti ostacolerà significativamente le prestazioni.

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

confronti delle prestazioni

Ho seguito ogni approccio e lo ho impacchettato in batch usando il seguente:

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

Ecco i risultati della durata totale, in millisecondi (ricorda che ogni volta include anche i comandi DBCC):

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

E l'ho fatto di nuovo senza i comandi 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

Rimozione di DBCC e loop, misurando solo un'iterazione non elaborata:

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

Infine, ho moltiplicato il numero di righe nella tabella di origine per 10 (cambiando la parte superiore a 50000 e aggiungendo un'altra tabella come cross join). I risultati di questo, una singola iterazione senza comandi DBCC (semplicemente nell'interesse del tempo):

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

Ho solo misurato la durata - lascerò al lettore un esercizio per confrontare questi approcci sui loro dati, confrontando altre metriche che possono essere importanti (o che possono variare con il loro schema / dati). Prima di trarre conclusioni da questa risposta, spetta a te testarlo in base ai tuoi dati e al tuo schema ... questi risultati cambieranno quasi sicuramente man mano che il numero di righe aumenta.


dimostrazione

Ho aggiunto un sqlfiddle . risultati:

inserisci qui la descrizione dell'immagine


conclusione

Nei miei test, la scelta sarebbe:

  1. Metodo di SQL Server 2012, se ho SQL Server 2012 disponibile.
  2. Se SQL Server 2012 non è disponibile e le mie date sono contigue, andrei con il metodo ricorsivo cte con date.
  3. Se né 1. né 2. sono applicabili, andrei con l'auto-join sull'aggiornamento stravagante, anche se le prestazioni erano vicine, solo perché il comportamento è documentato e garantito. Sono meno preoccupato per la compatibilità futura perché, si spera, se l'aggiornamento stravagante si interromperà sarà dopo che avrò già convertito tutto il mio codice in 1. :-)

Ma ancora una volta, dovresti testarli contro il tuo schema e i tuoi dati. Dato che si trattava di un test forzato con conteggi relativamente bassi, potrebbe anche essere una scoreggia nel vento. Ho fatto altri test con diversi schemi e conteggi delle righe e l'euristica delle prestazioni era abbastanza diversa ... ecco perché ho posto così tante domande di follow-up alla tua domanda originale.


AGGIORNARE

Ho scritto di più sul blog qui:

I migliori approcci per l'esecuzione dei totali - aggiornato per SQL Server 2012


1

Questa è, apparentemente, la soluzione ottimale

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

Qualche idea senza implementare una tabella temporanea (il mio proc sta già forzando i valori attraverso diverse tabelle temporanee per necessità, quindi sto cercando di trovare un modo per evitare di usare un'altra tabella temporanea)? In caso contrario, userò questo metodo. Penso che funzionerà

Può anche essere fatto con un self join o una subquery nidificata ma queste opzioni non funzionano altrettanto bene. Inoltre è probabile che tu colpisca tempdb comunque con queste alternative con alcuni spooling o worktable.

3
Basta essere consapevoli del fatto che questo metodo di "aggiornamento stravagante" non è garantito per funzionare: questa sintassi non è supportata e il suo comportamento non è definito e può essere interrotto in una versione futura, hot fix o service pack. Quindi, sebbene sì sia più veloce di alcune alternative supportate, ciò comporta un potenziale costo di compatibilità futuro.
Aaron Bertrand

6
Ci sono molti avvertimenti su questo approccio che Jeff Moden ha scritto da qualche parte. Ad esempio, dovresti avere un indice cluster day.
Martin Smith,

2
@MartinSmith È un articolo MOLTO GRANDE su sqlservercentral.com (vai alla pagina Autore e trova i suoi 'articoli sugli aggiornamenti di Quirck).
Fabricio Araujo,

-2

Solo un altro modo, costoso, ma indipendente dalla versione. Non utilizza tabelle o variabili temporanee.

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
Non va bene, è molto lento. Anche se hai solo 100 righe, eseguirà un ping-pong letto tra i tavoli 5.050 volte. 200 file, è 20.100 volte. Con solo 1.000 righe, passa esponenzialmente a 500.500 letture sqlblog.com/blogs/adam_machanic/archive/2006/07/12/…
Michael Buen,

Ho visto il link al tuo blog dopo aver pubblicato questo, ora vedo che questa è una pessima idea, grazie!
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.