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 RANGE
e ROWS
, o non la specifica, ROWS
evita 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:
conclusione
Nei miei test, la scelta sarebbe:
- Metodo di SQL Server 2012, se ho SQL Server 2012 disponibile.
- Se SQL Server 2012 non è disponibile e le mie date sono contigue, andrei con il metodo ricorsivo cte con date.
- 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
Day
una chiave e i valori sono contigui?