Calcola un totale parziale in SQL Server


170

Immagina la seguente tabella (chiamata 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

Vorrei una query che restituisca un totale parziale nell'ordine della data, come:

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

So che ci sono vari modi per farlo in SQL Server 2000/2005/2008.

Sono particolarmente interessato a questo tipo di metodo che utilizza il trucco aggregating-set-statement:

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

... questo è molto efficiente, ma ho sentito che ci sono dei problemi, perché non puoi necessariamente garantire che l' UPDATEistruzione elabori le righe nell'ordine corretto. Forse possiamo ottenere alcune risposte definitive su questo problema.

Ma forse ci sono altri modi in cui le persone possono suggerire?

modifica: ora con un SqlFiddle con la configurazione e l'esempio di 'aggiornamento trucco' sopra


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Aggiungi un ordine al tuo aggiornamento ... set e ottieni una garanzia.
Simon D,

Ma Order by non può essere applicato a un'istruzione UPDATE ... vero?
codeulike,

Vedi anche sqlperformance.com/2012/07/t-sql-queries/running-totals soprattutto se stai usando SQL Server 2012.
Aaron Bertrand

Risposte:


133

Aggiornamento , se si esegue SQL Server 2012, consultare: https://stackoverflow.com/a/10309947

Il problema è che l'implementazione di SQL Server della clausola Over è piuttosto limitata .

Oracle (e ANSI-SQL) ti consentono di fare cose come:

 SELECT somedate, somevalue,
  SUM(somevalue) OVER(ORDER BY somedate 
     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 
          AS RunningTotal
  FROM Table

SQL Server non offre una soluzione pulita a questo problema. Il mio istinto mi sta dicendo che questo è uno di quei rari casi in cui un cursore è il più veloce, anche se dovrò fare dei benchmark su grandi risultati.

Il trucco di aggiornamento è utile ma mi sembra abbastanza fragile. Sembra che se stai aggiornando una tabella completa, procederà nell'ordine della chiave primaria. Quindi, se imposti la data come chiave primaria crescente, probablysarai al sicuro. Ma stai facendo affidamento su un dettaglio dell'implementazione di SQL Server non documentato (anche se la query finisce per essere eseguita da due proc mi chiedo cosa accadrà, vedi: MAXDOP):

Campione di lavoro completo:

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

Hai chiesto un benchmark questo è il basso.

Il modo più veloce e sicuro per farlo sarebbe il Cursore, è un ordine di grandezza più veloce della sottoquery correlata del cross-join.

Il modo più veloce in assoluto è il trucco UPDATE. La mia unica preoccupazione è che non sono sicuro che in ogni caso l'aggiornamento procederà in modo lineare. Non c'è nulla nella query che lo dice esplicitamente.

In conclusione, per il codice di produzione andrei con il cursore.

Dati di test:

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

Test 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 

Test 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

Test 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

Test 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

1
Grazie. Quindi, il tuo esempio di codice è dimostrare che si somma nell'ordine della chiave primaria, presumo. Sarebbe interessante sapere se i cursori sono ancora più efficienti dei join per set di dati più grandi.
codeulike,

1
Ho appena testato il CTE @Martin, nulla si avvicina al trucco di aggiornamento - il cursore sembra inferiore sulle letture. Ecco una traccia del profiler i.stack.imgur.com/BbZq3.png
Sam Saffron,

3
@Martin Denali avrà una bella soluzione per questo msdn.microsoft.com/en-us/library/ms189461(v=SQL.110).aspx
Sam Saffron,

1
+1 per tutto il lavoro messo in questa risposta - Adoro l'opzione AGGIORNA; è possibile incorporare una partizione in questo script UPDATE? ad es. se fosse presente un campo aggiuntivo "Colore auto" questo script potrebbe restituire i totali in esecuzione all'interno di ogni partizione "Colore auto"?
whytheq,

2
la risposta iniziale (Oracle (e ANSI-SQL)) ora funziona in SQL Server 2017. Grazie, molto elegante!
DaniDev,

121

In SQL Server 2012 è possibile utilizzare SUM () con la clausola OVER () .

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

SQL Fiddle


40

Mentre Sam Saffron ha fatto un ottimo lavoro su di esso, non ha ancora fornito il codice di espressione di tabella comune ricorsivo per questo problema. E per noi che lavoriamo con SQL Server 2008 R2 e non con Denali, è ancora il modo più veloce per eseguire il totale, è circa 10 volte più veloce del cursore sul mio computer di lavoro per 100000 righe ed è anche una query incorporata.
Quindi, eccolo qui (suppongo che ci sia una ordcolonna nella tabella ed è un numero sequenziale senza spazi vuoti, per un'elaborazione veloce ci dovrebbe essere anche un vincolo univoco su questo numero):

;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

aggiornamento Ero anche curioso di questo aggiornamento con aggiornamento variabile o bizzarro . Quindi di solito funziona bene, ma come possiamo essere sicuri che funzioni ogni volta? bene, ecco un piccolo trucco (lo trovi qui - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258 ) - basta controllare l'attuale e il precedente orde utilizzare l' 1/0assegnazione nel caso in cui siano diversi da ciò che ti aspetti:

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

Da quello che ho visto se hai l'indice cluster / chiave primaria corretta sulla tua tabella (nel nostro caso sarebbe indice per ord_id) l'aggiornamento procederà in modo lineare per tutto il tempo (mai riscontrato dividere per zero). Detto questo, sta a te decidere se vuoi usarlo nel codice di produzione :)

aggiornamento 2 Sto collegando questa risposta, perché include alcune informazioni utili sull'inaffidabilità dell'aggiornamento bizzarro - nvarchar concatenation / index / nvarchar (max) inspiegabile comportamento .


6
Questa risposta merita un maggiore riconoscimento (o forse ha qualche difetto che non vedo?)
user1068352

dovrebbe esserci un numero sequenziale in modo da poter partecipare su ord = ord + 1 e talvolta ha bisogno di un po 'più di lavoro. Ma comunque, su SQL 2008 R2 sto usando questa soluzione
Roman Pekar

+1 Su SQLServer2008R2 preferisco anche l'approccio con CTE ricorsivo. Cordiali saluti, al fine di trovare il valore per le tabelle, che consentono spazi vuoti, utilizzo una sottoquery correlata. Aggiunge due ulteriori operazioni di ricerca alla query sqlfiddle.com/#!3/d41d8/18967
Aleksandr Fedorenko,

2
Nel caso in cui tu abbia già un ordinale per i tuoi dati e stai cercando una soluzione concisa (non cursore) basata su set su SQL 2008 R2, questo sembra essere perfetto.
Nick.McDermaid,

1
Non tutte le query totali in esecuzione avranno un campo ordinale contiguo. A volte un campo datetime è quello che hai, o i record sono stati eliminati dal mezzo dell'ordinamento. Questo potrebbe essere il motivo per cui non viene utilizzato più spesso.
Reuben,

28

L'operatore APPLY in SQL 2005 e versioni successive lavora per questo:

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

5
Funziona molto bene con set di dati più piccoli. Un aspetto negativo è che dovrete avere identiche clausole where nella query interna ed esterna.
Sire,

Poiché alcune delle mie date erano esattamente le stesse (fino alla frazione di secondo) ho dovuto aggiungere: row_number () over (ordine per txndate) alla tabella interna ed esterna e alcuni indici composti per farlo funzionare. Slick / soluzione semplice. A proposito, il test incrociato si applica contro la subquery ... è leggermente più veloce.
pghcpa,

questo è molto pulito e funziona bene con piccoli set di dati; più veloce del CTE ricorsivo
jtate,

questa è anche una buona soluzione (per piccoli set di dati), ma devi anche essere consapevole del fatto che implica che una determinata colonna sia unica
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

È inoltre possibile utilizzare la funzione ROW_NUMBER () e una tabella temporanea per creare una colonna arbitraria da utilizzare nel confronto sull'istruzione SELECT interna.


1
Questo è davvero inefficiente ... ma poi di nuovo non c'è un vero modo pulito per farlo in SQL Server
Sam Saffron,

Assolutamente è inefficiente, ma fa il lavoro e non c'è dubbio se qualcosa debba essere eseguito nell'ordine giusto o sbagliato.
Sam Axe

grazie, è utile avere risposte alternative e anche avere una critica efficace
codeulike

7

Utilizzare una query secondaria correlata. Molto semplice, ecco qui:

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

Il codice potrebbe non essere esattamente corretto, ma sono sicuro che l'idea sia.

GROUP BY è nel caso in cui una data appaia più di una volta, si vorrebbe vederla solo una volta nel set di risultati.

Se non ti dispiace vedere le date ricorrenti o vuoi vedere il valore e l'id originali, allora quello che vuoi è:

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

Grazie ... semplice è stato grandioso. C'era un indice da aggiungere per le prestazioni, ma era abbastanza semplice, (prendendo uno dei consigli da Database Engine Tuning Advisor;), e poi ha funzionato come un colpo.
Doug_Ivison,


4

Supponendo che le finestre funzionino su SQL Server 2008 come altrove (che ho provato), provalo:

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

MSDN dice che è disponibile in SQL Server 2008 (e forse anche nel 2005?) Ma non ho un'istanza a portata di mano per provarlo.

EDIT: beh, apparentemente SQL Server non consente una specifica della finestra ("OVER (...)") senza specificare "PARTITION BY" (dividendo il risultato in gruppi ma non aggregando esattamente come GROUP BY). Fastidioso: il riferimento alla sintassi MSDN suggerisce che è facoltativo, ma al momento ho solo istanze di SqlServer 2000.

La query che ho fornito funziona sia in Oracle 10.2.0.3.0 che in PostgreSQL 8.4-beta. Quindi dì a MS di recuperare;)


2
L'uso di OVER con SUM non funzionerà in questo caso per un totale parziale. La clausola OVER non accetta ORDER BY quando viene utilizzata con SUM. Devi usare PARTITION BY, che non funzionerà per eseguire i totali.
Sam Axe

grazie, in realtà è utile sapere perché questo non funzionerà. araqnid forse potresti modificare la tua risposta per spiegare perché non è un'opzione
codeulike


Questo in realtà funziona per me, perché ho bisogno di partizionare - quindi anche se questa non è la risposta più popolare, è la soluzione più semplice al mio problema con RT in SQL.
William MB,

Non ho MSSQL 2008 con me, ma penso che potresti probabilmente partizionare (seleziona null) e aggirare il problema del partizionamento. Oppure fai una sottoselezione 1 partitionmee partiziona con quella. Inoltre, la partizione è probabilmente necessaria nelle situazioni di vita reale quando si fanno i rapporti.
Nurettin,

4

Se si utilizza il server SQL 2008 R2 sopra. Quindi, sarebbe il modo più breve per fare;

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

RITARDO viene utilizzato per ottenere il valore della riga precedente. Puoi fare google per maggiori informazioni.

[1]:


1
Credo che il LAG esista solo in SQL Server 2012 e versioni successive (non nel 2008)
AaA

1
L'uso di LAG () non migliora SUM(somevalue) OVER(...) che mi sembra molto più pulito
Used_By_Al già

2

Credo che un totale parziale possa essere raggiunto usando la semplice operazione INNER JOIN di seguito.

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

Sì, penso che questo sia equivalente a "Test 3" nella risposta di Sam Saffron.
codeulike,

2

Quanto segue produrrà i risultati richiesti.

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

Avere un indice cluster su SomeDate migliorerà notevolmente le prestazioni.


@Dave Penso che questa domanda stia cercando di trovare un modo efficace per farlo, il cross-join sarà molto lento per grandi set
Sam Saffron,

grazie, è utile avere risposte alternative e anche avere una critica efficace
codeulike,


2

Sebbene il modo migliore per farlo sia usare una funzione di finestra, può anche essere fatto usando una semplice query secondaria correlata .

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;

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

Probabilmente dovresti fornire alcune informazioni su ciò che stai facendo qui e annotare eventuali vantaggi / svantaggi di questo particolare metodo.
TT.

0

Ecco 2 semplici modi per calcolare il totale parziale:

Approccio 1 : può essere scritto in questo modo se il DBMS supporta le funzioni analitiche

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

Approccio 2 : è possibile utilizzare OUTER APPLY se la versione del database / DBMS stesso non supporta le funzioni analitiche

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 devi calcolare il totale parziale per diverse partizioni separatamente, puoi farlo come indicato qui: Calcolo dei totali correnti su più righe e raggruppamento per ID

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.