Calcular um total em execução no SQL Server

170

Imagine a seguinte tabela (chamada TestTable):

id     somedate    somevalue
--     --------    ---------
45     01/Jan/09   3
23     08/Jan/09   5
12     02/Feb/09   0
77     14/Feb/09   7
39     20/Feb/09   34
33     02/Mar/09   6

Gostaria de uma consulta que retorne um total em execução na ordem da data, como:

id     somedate    somevalue  runningtotal
--     --------    ---------  ------------
45     01/Jan/09   3          3
23     08/Jan/09   5          8
12     02/Feb/09   0          8
77     14/Feb/09   7          15  
39     20/Feb/09   34         49
33     02/Mar/09   6          55

Eu sei que existem várias maneiras de fazer isso no SQL Server 2000/2005/2008.

Estou particularmente interessado nesse tipo de método que usa o truque de agregação de conjunto de instruções:

INSERT INTO @AnotherTbl(id, somedate, somevalue, runningtotal) 
   SELECT id, somedate, somevalue, null
   FROM TestTable
   ORDER BY somedate

DECLARE @RunningTotal int
SET @RunningTotal = 0

UPDATE @AnotherTbl
SET @RunningTotal = runningtotal = @RunningTotal + somevalue
FROM @AnotherTbl

... isso é muito eficiente, mas ouvi dizer que há problemas em torno disso, porque você não pode necessariamente garantir que a UPDATEinstrução processe as linhas na ordem correta. Talvez possamos obter algumas respostas definitivas sobre esse problema.

Mas talvez haja outras maneiras que as pessoas possam sugerir?

edit: Agora com um SqlFiddle com a instalação e o exemplo 'truque de atualização' acima

codeulike
fonte
blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Adicione um pedido ao seu conjunto de atualizações ... e você terá uma garantia.
296 Simon D
Mas Order by não pode ser aplicado a uma instrução UPDATE ... pode?
codeulike
Ver também sqlperformance.com/2012/07/t-sql-queries/running-totals especialmente se você estiver usando SQL Server 2012.
Aaron Bertrand

Respostas:

133

Atualização , 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 fornece uma solução limpa para esse problema. Meu instinto está me dizendo que esse é um daqueles casos raros em que um cursor é o mais rápido, embora eu tenha que fazer alguns testes comparativos de grandes resultados.

O truque de atualização é útil, mas eu sinto que é bastante frágil. Parece que se você estiver atualizando uma tabela completa, ela prosseguirá na ordem da chave primária. Portanto, se você definir sua data como uma chave primária ascendente, probablyestará seguro. Mas você está confiando em um detalhe de implementação não documentado do SQL Server (também se a consulta acabar sendo executada por dois procs, imagino o que acontecerá, consulte: 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 inicial.

A maneira mais rápida e segura de fazer isso seria o Cursor, é uma ordem de magnitude mais rápida que a subconsulta correlacionada da junção cruzada.

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

Bottom line, para o código de produção eu iria com 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
Sam Saffron
fonte
1
Obrigado. Portanto, seu exemplo de código é demonstrar que será somado na ordem da chave primária, presumo. Seria interessante saber se os cursores ainda são mais eficientes do que as junções para conjuntos de dados maiores.
Codeulike 14/05
1
Acabei de testar o CTE @Martin, nada chega perto do truque de atualização - o cursor parece mais baixo nas leituras. Aqui está um rastreamento do profiler i.stack.imgur.com/BbZq3.png
Sam Saffron
3
@ Martin Denali vai ter uma solução agradável bastante para este msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
Sam Saffron
1
+1 para todo o trabalho colocado nesta resposta - adoro a opção UPDATE; uma partição pode ser incorporada nesse script UPDATE? por exemplo, se houvesse um campo adicional "Cor do carro", esse script poderia retornar totais em execução em cada partição "Cor do carro"?
whytheq
2
a resposta inicial (Oracle (e ANSI-SQL)) agora funciona no SQL server 2017. Obrigado, muito elegante!
DaniDev 24/09/19
121

No SQL Server 2012, você pode usar SUM () com a cláusula OVER () .

select id,
       somedate,
       somevalue,
       sum(somevalue) over(order by somedate rows unbounded preceding) as runningtotal
from TestTable

SQL Fiddle

Mikael Eriksson
fonte
40

Embora Sam Saffron tenha feito um ótimo trabalho, ele ainda não forneceu código de expressão de tabela comum recursivo para esse problema. E para nós que trabalhamos com o SQL Server 2008 R2 e não com o Denali, ainda é a maneira mais rápida de executar o total, é cerca de 10 vezes mais rápido que o cursor no meu computador de trabalho por 100000 linhas, e também é uma consulta embutida.
Então, aqui está (suponho que exista uma ordcoluna na tabela e seu número seqüencial sem lacunas, para um processamento rápido, também deve haver uma restrição exclusiva nesse número):

;with 
CTE_RunningTotal
as
(
    select T.ord, T.total, T.total as running_total
    from #t as T
    where T.ord = 0
    union all
    select T.ord, T.total, T.total + C.running_total as running_total
    from CTE_RunningTotal as C
        inner join #t as T on T.ord = C.ord + 1
)
select C.ord, C.total, C.running_total
from CTE_RunningTotal as C
option (maxrecursion 0)

-- CPU 140, Reads 110014, Duration 132

sql fiddle demo

update Eu também estava curioso sobre esta atualização com atualização variável ou peculiar . Então geralmente funciona bem, mas como podemos ter certeza de que funciona sempre? bem, aqui está um pequeno truque (encontrado aqui - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - basta verificar a corrente e a anterior orde usar a 1/0atribuição caso sejam diferentes do que você está esperando:

declare @total int, @ord int

select @total = 0, @ord = -1

update #t set
    @total = @total + total,
    @ord = case when ord <> @ord + 1 then 1/0 else ord end,
    ------------------------
    running_total = @total

select * from #t

-- CPU 0, Reads 58, Duration 139

Pelo que vi, se você possui um índice / chave primária em cluster adequado em sua tabela (no nosso caso, seria um índice por ord_id), a atualização continuará de maneira linear o tempo todo (nunca foi encontrado dividir por zero). Dito isto, você decide se deseja usá-lo no código de produção :)

atualização 2 Estou vinculando esta resposta, pois inclui algumas informações úteis sobre a confiabilidade da atualização peculiar - comportamento inexplicável da concatenação nvarchar / índice / nvarchar (max) .

Roman Pekar
fonte
6
Essa resposta merece mais reconhecimento (ou talvez ele tenha alguma falha que eu não vejo?)
user1068352
deve haver um número seqüencial para que você possa ingressar em ord = ord + 1 e às vezes ele precisa de um pouco mais de trabalho. Mas de qualquer maneira, no SQL 2008 R2 Estou usando esta solução
Roman Pekar
+1 No SQLServer2008R2, também prefiro a abordagem com CTE recursiva. Para sua informação, a fim de encontrar o valor para as tabelas, que permitem lacunas, eu uso uma subconsulta correlacionada. Acrescenta duas operações adicionais procuram a consulta sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko
2
Para o caso em que você já possui um ordinal para seus dados e procura uma solução concisa (sem cursor) baseada em conjunto no SQL 2008 R2, isso parece perfeito.
Nick.McDermaid
1
Nem toda consulta total em execução terá um campo ordinal que é contíguo. Às vezes, você tem um campo de data e hora ou os registros foram excluídos do meio da classificação. Pode ser por isso que não é usado com mais frequência.
Reuben
28

O operador APPLY no SQL 2005 e superior trabalha para isso:

select
    t.id ,
    t.somedate ,
    t.somevalue ,
    rt.runningTotal
from TestTable t
 cross apply (select sum(somevalue) as runningTotal
                from TestTable
                where somedate <= t.somedate
            ) as rt
order by t.somedate
Mike Forman
fonte
5
Funciona muito bem para conjuntos de dados menores. Uma desvantagem é que você precisará ter cláusulas where idênticas na consulta interna e externa.
Sire
Como algumas das minhas datas eram exatamente as mesmas (até a fração de segundo), tive que adicionar: row_number () over (ordem por txndate) à tabela interna e externa e alguns índices compostos para fazê-la funcionar. Solução simples / lisa. Aliás, o teste cruzado se aplica à subconsulta ... é um pouco mais rápido.
Pgcpa
isso é muito limpo e funciona bem com pequenos conjuntos de dados; mais rápido que o recursiva CTE
jtate
isso é bom solução bem (para pequenos conjuntos de dados), mas você também tem que estar ciente de que isso implica coluna somedate de ser único
Roman Pekar
11
SELECT TOP 25   amount, 
    (SELECT SUM(amount) 
    FROM time_detail b 
    WHERE b.time_detail_id <= a.time_detail_id) AS Total FROM time_detail a

Você também pode usar a função ROW_NUMBER () e uma tabela temporária para criar uma coluna arbitrária para usar na comparação na instrução SELECT interna.

Sam Axe
fonte
1
Isto é realmente ineficiente ... mas, novamente não há nenhuma maneira real limpa de fazer isso no sql server
Sam Saffron
Absolutamente é ineficiente - mas faz o trabalho e não há dúvida de que algo deve ser executado na ordem certa ou errada.
Sam Axe
graças, sua útil ter respostas alternativas, e também útil ter crítica efficienty
codeulike
7

Use uma subconsulta correlacionada. Muito simples, aqui está:

SELECT 
somedate, 
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
GROUP BY somedate
ORDER BY somedate

O código pode não estar exatamente correto, mas tenho certeza de que a ideia é.

O GROUP BY é caso uma data apareça mais de uma vez, você só quer vê-la uma vez no conjunto de resultados.

Se você não se importa em ver datas repetidas ou deseja ver o valor e o ID originais, deseja o seguinte:

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate
KthProg
fonte
Obrigado ... simples foi ótimo. Havia um índice a ser adicionado ao desempenho, mas isso era bastante simples (seguindo uma das recomendações do Orientador de Otimização do Mecanismo de Banco de Dados;) e, em seguida, foi executado como um tiro.
Doug_Ivison
4

Supondo que a janela funcione no SQL Server 2008 como em outros lugares (que eu tentei), experimente:

select testtable.*, sum(somevalue) over(order by somedate)
from testtable
order by somedate;

O MSDN diz que está disponível no SQL Server 2008 (e talvez também em 2005?), Mas não tenho uma instância disponível para testá-lo.

EDIT: bem, aparentemente o SQL Server não permite uma especificação de janela ("OVER (...)") sem especificar "PARTITION BY" (dividindo o resultado em grupos, mas não agregando da mesma maneira que o GROUP BY). Irritante - a referência de sintaxe do MSDN sugere que isso é opcional, mas eu só tenho instâncias do SqlServer 2000 no momento.

A consulta que forneci funciona no Oracle 10.2.0.3.0 e no PostgreSQL 8.4-beta. Então diga à MS para recuperar o atraso;)

araqnid
fonte
2
Usar OVER com SUM não funcionará neste caso para fornecer um total em execução. A cláusula OVER não aceita ORDER BY quando usado com SUM. Você precisa usar PARTITION BY, que não funcionará para totais em execução.
Sam Axe
obrigado, é realmente útil saber por que isso não funciona. araqnid talvez você poderia editar sua resposta para explicar por que não é uma opção
codeulike
Isso realmente funciona para mim, porque preciso particionar - mesmo que essa não seja a resposta mais popular, é a solução mais fácil para o meu problema de RT no SQL.
William MB
Eu não tenho o MSSQL 2008 comigo, mas acho que você provavelmente poderia particionar (selecione nulo) e solucionar o problema de particionamento. Ou faça uma subseleção com 1 partitionmee particione com isso. Além disso, a partição por é provavelmente necessária em situações da vida real ao fazer relatórios.
nurettin
4

Se você estiver usando o Sql server 2008 R2 acima. Então, seria a maneira mais curta de fazer;

Select id
    ,somedate
    ,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable 

LAG é usado para obter o valor da linha anterior. Você pode fazer o google para mais informações.

[1]:

shambhu yadav
fonte
1
Acredito LAG só existe no servidor SQL 2012 e superior (não 2008)
AaA
1
Usando GAL () não melhoram com SUM(somevalue) OVER(...) o que parece um aspirador muito para mim
Used_By_Already
2

Acredito que um total em execução possa ser alcançado usando a operação simples INNER JOIN abaixo.

SELECT
     ROW_NUMBER() OVER (ORDER BY SomeDate) AS OrderID
    ,rt.*
INTO
    #tmp
FROM
    (
        SELECT 45 AS ID, CAST('01-01-2009' AS DATETIME) AS SomeDate, 3 AS SomeValue
        UNION ALL
        SELECT 23, CAST('01-08-2009' AS DATETIME), 5
        UNION ALL
        SELECT 12, CAST('02-02-2009' AS DATETIME), 0
        UNION ALL
        SELECT 77, CAST('02-14-2009' AS DATETIME), 7
        UNION ALL
        SELECT 39, CAST('02-20-2009' AS DATETIME), 34
        UNION ALL
        SELECT 33, CAST('03-02-2009' AS DATETIME), 6
    ) rt

SELECT
     t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
    ,SUM(t2.SomeValue) AS RunningTotal
FROM
    #tmp t1
    JOIN #tmp t2
        ON t2.OrderID <= t1.OrderID
GROUP BY
     t1.OrderID
    ,t1.ID
    ,t1.SomeDate
    ,t1.SomeValue
ORDER BY
    t1.OrderID

DROP TABLE #tmp
clevster
fonte
Sim, acho que isso é equivalente ao 'Teste 3' na resposta de Sam Saffron.
precisa saber é o seguinte
2

O seguinte produzirá os resultados necessários.

SELECT a.SomeDate,
       a.SomeValue,
       SUM(b.SomeValue) AS RunningTotal
FROM TestTable a
CROSS JOIN TestTable b
WHERE (b.SomeDate <= a.SomeDate) 
GROUP BY a.SomeDate,a.SomeValue
ORDER BY a.SomeDate,a.SomeValue

Ter um índice em cluster no SomeDate melhorará bastante o desempenho.

Dave Barker
fonte
@ Dave Eu acho que esta questão está a tentar encontrar uma maneira eficiente de fazer isso, atravesse juntando vai ser muito lento para grandes conjuntos
Sam Saffron
graças, sua útil ter respostas alternativas, e também útil ter crítica efficienty
codeulike
2

Embora a melhor maneira de fazer isso seja usar uma função de janela, isso também pode ser feito usando uma simples consulta secundária correlacionada .

Select id, someday, somevalue, (select sum(somevalue) 
                                from testtable as t2
                                where t2.id = t1.id
                                and t2.someday <= t1.someday) as runningtotal
from testtable as t1
order by id,someday;
Krahul3
fonte
0
BEGIN TRAN
CREATE TABLE #Table (_Id INT IDENTITY(1,1) ,id INT ,    somedate VARCHAR(100) , somevalue INT)


INSERT INTO #Table ( id  ,    somedate  , somevalue  )
SELECT 45 , '01/Jan/09', 3 UNION ALL
SELECT 23 , '08/Jan/09', 5 UNION ALL
SELECT 12 , '02/Feb/09', 0 UNION ALL
SELECT 77 , '14/Feb/09', 7 UNION ALL
SELECT 39 , '20/Feb/09', 34 UNION ALL
SELECT 33 , '02/Mar/09', 6 

;WITH CTE ( _Id, id  ,  _somedate  , _somevalue ,_totvalue ) AS
(

 SELECT _Id , id  ,    somedate  , somevalue ,somevalue
 FROM #Table WHERE _id = 1
 UNION ALL
 SELECT #Table._Id , #Table.id  , somedate  , somevalue , somevalue + _totvalue
 FROM #Table,CTE 
 WHERE #Table._id > 1 AND CTE._Id = ( #Table._id-1 )
)

SELECT * FROM CTE

ROLLBACK TRAN
Mansoor
fonte
Provavelmente, você deve fornecer algumas informações sobre o que está fazendo aqui e observar as vantagens / desvantagens deste método específico.
TT.
0

Aqui estão duas maneiras simples de calcular o total em execução:

Abordagem 1 : pode ser escrito desta maneira se o seu DBMS suportar funções analíticas

SELECT     id
           ,somedate
           ,somevalue
           ,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM       TestTable

Abordagem 2 : você pode usar OUTER APPLY se a versão do banco de dados / DBMS em si não suportar funções analíticas

SELECT     T.id
           ,T.somedate
           ,T.somevalue
           ,runningtotal = OA.runningtotal
FROM       TestTable T
           OUTER APPLY (
                           SELECT   runningtotal = SUM(TI.somevalue)
                           FROM     TestTable TI
                           WHERE    TI.somedate <= S.somedate
                       ) OA;

Nota: - Se você precisar calcular o total de execução para diferentes partições separadamente, isso pode ser feito conforme publicado aqui: Calculando totais de execução em linhas e agrupando por ID

san
fonte