Aqui estão alguns métodos que você pode comparar. Primeiro vamos configurar uma tabela com alguns dados fictícios. Estou preenchendo isso com um monte de dados aleatórios de sys.all_columns. Bem, é meio aleatório - estou garantindo que as datas sejam contíguas (o que é realmente importante apenas para uma das respostas).
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;
Resultados:
Earliest_Day Latest_Day Unique_Days Total_Days
------------------- ------------------- ----------- ----------
2012-05-01 00:00:00 2013-09-13 00:00:00 501 5000
Os dados são assim (5000 linhas) - mas parecerão um pouco diferentes no seu sistema, dependendo da versão e número da compilação:
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 os resultados dos totais em execução devem ficar assim (501 linhas):
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
...
Portanto, os métodos que vou comparar são:
- "auto-junção" - a abordagem purista baseada em conjuntos
- "CTE recursiva com datas" - isso depende de datas contíguas (sem lacunas)
- "CTE recursivo com número de linha" - semelhante ao acima, mas mais lento, com base em ROW_NUMBER
- "CTE recursivo com #temp table" - roubado da resposta de Mikael, conforme sugerido
- "atualização peculiar" que, embora não seja suportada e não prometa um comportamento definido, parece bastante popular
- "cursor"
- SQL Server 2012 usando a nova funcionalidade de janelas
auto-junção
É assim que as pessoas o instruem a fazê-lo quando o alertam para ficar longe dos cursores, porque "baseado em conjuntos é sempre mais rápido". Em algumas experiências recentes, descobri que o cursor ultrapassa essa solução.
;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 recursivo com datas
Lembrete - isso depende de datas contíguas (sem intervalos), até 10000 níveis de recursão e que você saiba a data de início do intervalo em que está interessado (para definir a âncora). Você pode definir a âncora dinamicamente usando uma subconsulta, é claro, mas eu queria manter as coisas simples.
;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 recursivo com row_number
O cálculo do número da linha é um pouco caro aqui. Novamente, isso suporta o nível máximo de recursão de 10000, mas você não precisa atribuir a âncora.
;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 recursivo com tabela temporária
Roubo da resposta de Mikael, como sugerido, para incluí-lo nos testes.
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;
atualização peculiar
Mais uma vez, estou incluindo isso apenas por completude; Pessoalmente, eu não confiaria nessa solução, pois, como mencionei em outra resposta, não é garantido que este método funcione e pode ser completamente interrompido em uma versão futura do SQL Server. (Estou fazendo o possível para forçar o SQL Server a obedecer a ordem que desejo, usando uma dica para a escolha do índice.)
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;
cursor
"Cuidado, existem cursores aqui! Cursores são maus! Você deve evitar cursores a todo custo!" Não, isso não sou eu falando, são coisas que eu ouço muito. Ao contrário da opinião popular, existem alguns casos em que os cursores são apropriados.
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 você está na versão mais recente do SQL Server, os aprimoramentos na funcionalidade de janelas permitem calcular facilmente os totais em execução sem o custo exponencial da auto-associação (o SUM é calculado em uma passagem), a complexidade dos CTEs (incluindo o requisito de linhas contíguas para o melhor desempenho da CTE), a atualização peculiar não suportada e o cursor proibido. Apenas tenha cuidado com a diferença entre usar RANGE
e ROWS
, ou não especificar, apenas ROWS
evita um spool no disco, o que dificultará significativamente o desempenho.
;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];
comparações de desempenho
Peguei cada abordagem e agrupei um lote usando o seguinte:
SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();
Aqui estão os resultados da duração total, em milissegundos (lembre-se de incluir também os comandos DBCC sempre):
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 fiz de novo sem os comandos 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
Removendo o DBCC e os loops, apenas medindo uma iteração bruta:
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
Finalmente, multipliquei a contagem de linhas na tabela de origem por 10 (alterando o topo para 50000 e adicionando outra tabela como uma junção cruzada). Os resultados disso, uma única iteração sem comandos DBCC (simplesmente no interesse do 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
Apenas medi a duração - deixarei como um exercício para o leitor comparar essas abordagens em seus dados, comparando outras métricas que podem ser importantes (ou podem variar de acordo com o esquema / dados). Antes de tirar conclusões dessa resposta, você deve testá-lo com base em seus dados e seu esquema ... esses resultados quase certamente mudarão à medida que a contagem de linhas aumentar.
demonstração
Eu adicionei um sqlfiddle . Resultados:
conclusão
Nos meus testes, a escolha seria:
- Método do SQL Server 2012, se eu tiver o SQL Server 2012 disponível.
- Se o SQL Server 2012 não estiver disponível e minhas datas forem contíguas, eu usaria o método recursive cte with datas.
- Se nem 1. nem 2. são aplicáveis, eu faria a auto-junção sobre a atualização peculiar, mesmo que o desempenho estivesse próximo, apenas porque o comportamento está documentado e garantido. Estou menos preocupado com a compatibilidade futura, porque, esperançosamente, se a atualização for interrompida, será depois que eu já tiver convertido todo o meu código para 1. :-)
Mas, novamente, você deve testá-los no seu esquema e dados. Como este foi um teste artificial com contagens de linha relativamente baixas, também pode ser um peido ao vento. Fiz outros testes com contagens diferentes de esquema e linha, e as heurísticas de desempenho eram bem diferentes ... e foi por isso que fiz tantas perguntas de acompanhamento à sua pergunta original.
ATUALIZAR
Eu escrevi mais sobre isso aqui:
Melhores abordagens para execução de totais - atualizado para o SQL Server 2012
Day
uma chave e os valores são contíguos?