Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Calcular um total em execução no SQL Server


Atualizar , se você estiver executando o SQL Server 2012, consulte:https://stackoverflow.com/a/10309947

O problema é que a implementação do SQL Server da cláusula Over é um pouco limitada.

Oracle (e ANSI-SQL) permitem que você faça coisas como:
 SELECT somedate, somevalue,
  SUM(somevalue) OVER(ORDER BY somedate 
     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 
          AS RunningTotal
  FROM Table

O SQL Server não oferece uma solução limpa para esse problema. Meu instinto está me dizendo que este é um daqueles raros casos em que um cursor é o mais rápido, embora eu tenha que fazer alguns benchmarks em grandes resultados.

O truque de atualização é útil, mas sinto que é bastante frágil. Parece que, se você estiver atualizando uma tabela completa, ela continuará na ordem da chave primária. Portanto, se você definir sua data como uma chave primária ascendente, probably esteja a salvo. Mas você está contando com um detalhe de implementação do SQL Server não documentado (também se a consulta acabar sendo executada por dois procs eu me pergunto o que vai acontecer, veja:MAXDOP):

Amostra de trabalho completa:
drop table #t 
create table #t ( ord int primary key, total int, running_total int)

insert #t(ord,total)  values (2,20)
-- notice the malicious re-ordering 
insert #t(ord,total) values (1,10)
insert #t(ord,total)  values (3,10)
insert #t(ord,total)  values (4,1)

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t
order by ord 

ord         total       running_total
----------- ----------- -------------
1           10          10
2           20          30
3           10          40
4           1           41

Você pediu um benchmark, este é o ponto baixo.

A maneira SAFE mais rápida de fazer isso seria o Cursor, é uma ordem de magnitude mais rápida do que a subconsulta correlacionada de cross-join.

A maneira mais rápida absoluta é o truque UPDATE. Minha única preocupação com isso é que não tenho certeza de que, em todas as circunstâncias, a atualização ocorra de maneira linear. Não há nada na consulta que diga isso explicitamente.

Resumindo, para código de produção, eu usaria o cursor.

Dados de teste:
create table #t ( ord int primary key, total int, running_total int)

set nocount on 
declare @i int
set @i = 0 
begin tran
while @i < 10000
begin
   insert #t (ord, total) values (@i,  rand() * 100) 
    set @i = @i +1
end
commit

Teste 1:
SELECT ord,total, 
    (SELECT SUM(total) 
        FROM #t b 
        WHERE b.ord <= a.ord) AS b 
FROM #t a

-- CPU 11731, Reads 154934, Duration 11135 

Teste 2:
SELECT a.ord, a.total, SUM(b.total) AS RunningTotal 
FROM #t a CROSS JOIN #t b 
WHERE (b.ord <= a.ord) 
GROUP BY a.ord,a.total 
ORDER BY a.ord

-- CPU 16053, Reads 154935, Duration 4647

Teste 3:
DECLARE @TotalTable table(ord int primary key, total int, running_total int)

DECLARE forward_cursor CURSOR FAST_FORWARD 
FOR 
SELECT ord, total
FROM #t 
ORDER BY ord


OPEN forward_cursor 

DECLARE @running_total int, 
    @ord int, 
    @total int
SET @running_total = 0

FETCH NEXT FROM forward_cursor INTO @ord, @total 
WHILE (@@FETCH_STATUS = 0)
BEGIN
     SET @running_total = @running_total + @total
     INSERT @TotalTable VALUES(@ord, @total, @running_total)
     FETCH NEXT FROM forward_cursor INTO @ord, @total 
END

CLOSE forward_cursor
DEALLOCATE forward_cursor

SELECT * FROM @TotalTable

-- CPU 359, Reads 30392, Duration 496

Teste 4:
declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t

-- CPU 0, Reads 58, Duration 139