Se você não precisar ARMAZENAR os dados (o que você não deveria, porque você precisa atualizar os totais em execução sempre que qualquer linha for alterada, adicionada ou excluída), e se você não confiar na atualização peculiar (que você não deveria, porque não é garantido que funcione e seu comportamento pode mudar com um hotfix, service pack, atualização ou mesmo uma alteração de índice ou estatística subjacente), você pode tentar esse tipo de consulta em tempo de execução. Este é um método que o colega MVP Hugo Kornelis cunhou "iteração baseada em conjunto" (ele postou algo semelhante em um de seus capítulos de Mergulhos profundos do MVP do SQL Server ). Como a execução de totais normalmente requer um cursor sobre todo o conjunto, uma atualização peculiar sobre todo o conjunto ou uma única autojunção não linear que se torna cada vez mais cara à medida que a contagem de linhas aumenta, o truque aqui é percorrer alguns elemento no conjunto (neste caso, a "classificação" de cada linha em termos de mês, para cada usuário - e você processa apenas cada classificação uma vez para todas as combinações de usuário/mês nessa classificação, portanto, em vez de percorrer 200.000 linhas, você faz um loop até 24 vezes).
DECLARE @t TABLE
(
[user_id] INT,
[month] TINYINT,
total DECIMAL(10,1),
RunningTotal DECIMAL(10,1),
Rnk INT
);
INSERT @t SELECT [user_id], [month], total, total,
RANK() OVER (PARTITION BY [user_id] ORDER BY [month])
FROM dbo.my_table;
DECLARE @rnk INT = 1, @rc INT = 1;
WHILE @rc > 0
BEGIN
SET @rnk += 1;
UPDATE c SET RunningTotal = p.RunningTotal + c.total
FROM @t AS c INNER JOIN @t AS p
ON c.[user_id] = p.[user_id]
AND p.rnk = @rnk - 1
AND c.rnk = @rnk;
SET @rc = @@ROWCOUNT;
END
SELECT [user_id], [month], total, RunningTotal
FROM @t
ORDER BY [user_id], rnk;
Resultados:
user_id month total RunningTotal
------- ----- ----- ------------
1 1 2.0 2.0
1 2 1.0 3.0
1 3 3.5 6.5 -- I think your calculation is off
2 1 0.5 0.5
2 2 1.5 2.0
2 3 2.0 4.0
Claro que você pode atualize a tabela base a partir dessa variável de tabela, mas por que se preocupar, já que esses valores armazenados só são bons até a próxima vez que a tabela for tocada por qualquer instrução DML?
UPDATE mt
SET cumulative_total = t.RunningTotal
FROM dbo.my_table AS mt
INNER JOIN @t AS t
ON mt.[user_id] = t.[user_id]
AND mt.[month] = t.[month];
Como não estamos contando com nenhum tipo de ordenação implícita, isso é 100% compatível e merece uma comparação de desempenho em relação à atualização peculiar não suportada. Mesmo que não o supere, mas chegue perto, você deve considerar usá-lo de qualquer maneira IMHO.
Quanto à solução SQL Server 2012, Matt menciona
RANGE
mas como esse método usa um spool em disco, você também deve testar com ROWS
em vez de apenas executar com RANGE
. Aqui está um exemplo rápido para o seu caso:SELECT
[user_id],
[month],
total,
RunningTotal = SUM(total) OVER
(
PARTITION BY [user_id]
ORDER BY [month] ROWS UNBOUNDED PRECEDING
)
FROM dbo.my_table
ORDER BY [user_id], [month];
Compare isso com
RANGE UNBOUNDED PRECEDING
ou nenhum ROWS\RANGE
em tudo (que também usará o RANGE
carretel no disco). O acima terá duração geral e caminho mais baixos menos E/S, embora o plano pareça um pouco mais complexo (um operador de projeto de sequência adicional). Recentemente, publiquei uma postagem no blog descrevendo algumas diferenças de desempenho que observei para um cenário específico de totais em execução:
http://www.sqlperformance.com/2012/07 /t-sql-queries/running-totals