Calculer un total en cours D'exécution dans SQL Server

Imaginez le tableau suivant (appelé 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

Je voudrais une requête qui renvoie un total en cours d'exécution dans l'ordre des dates, comme:

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

Je sais qu'il y a différentes façons de le faire dans SQL Server 2000 / 2005 / 2008.

Je suis particulièrement intéressé par ce genre de méthode qui utilise l'agrégation-set-déclaration astuce:

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

... c'est très efficace mais j'ai entendu dire qu'il y avait des problèmes à ce sujet parce que vous ne pouvez pas nécessairement le garantir l'instruction UPDATE traitera les lignes dans le bon ordre. Peut-être que nous pouvons obtenir des réponses définitives à ce sujet.

Mais peut-être y a-t-il d'autres façons que les gens peuvent suggérer?

Edit: maintenant avec un SqlFiddle avec l'installation et l'exemple 'update trick' ci-dessus

143
demandé sur marc_s 2009-05-14 03:42:26

14 réponses

Mise à Jour, si vous exécutez SQL Server 2012 voir: https://stackoverflow.com/a/10309947

Le problème est que L'implémentation SQL Server de la clause Over est quelque peu limitée .

Oracle (et ANSI-SQL) vous permettent de faire des choses comme:

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

SQL Server ne vous donne aucune solution propre à ce problème. Mon instinct me dit que c'est l'un de ces rares cas où un curseur est le plus rapide, bien que je doive en faire benchmarking sur les grands résultats.

L'astuce de mise à jour est pratique mais je me sens assez fragile. Il semble que si vous mettez à jour une table complète, elle se déroulera dans l'ordre de la clé primaire. Donc, si vous définissez votre date comme une clé primaire ascendante, vous serez probably en sécurité. Mais vous comptez sur un détail D'implémentation SQL Server non documenté (aussi si la requête finit par être effectuée par deux procs, je me demande ce qui va se passer, voir: MAXDOP):

Plein échantillon de travail:

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

Vous avez demandé un benchmark c'est la vérité.

Le moyen le plus rapide et le plus sûr de le faire serait le curseur, c'est un ordre de grandeur plus rapide que la sous-requête corrélée de CROSS-join.

Le moyen le plus rapide absolu est l'astuce de mise à jour. Ma seule préoccupation est que je ne suis pas certain que, dans toutes les circonstances, la mise à jour se déroulera de manière linéaire. Il n'y a rien dans la requête qui le dit explicitement.

Résultat Net, pour le code de production I serait aller avec le curseur.

Données D'essai:

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

Essai 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 

Essai 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

Essai 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

Essai 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
109
répondu Sam Saffron 2017-05-23 11:55:07

Dans SQL Server 2012, vous pouvez utiliser SUM() avec SUR() clause.

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

SQL Violon

101
répondu Mikael Eriksson 2012-06-13 11:21:13

Alors que Sam Saffron a fait un excellent travail, il n'a toujours pas fourni recursive common table expression code pour ce problème. Et pour nous qui travaillons avec SQL Server 2008 R2 et non Denali, c'est toujours le moyen le plus rapide d'exécuter total, c'est environ 10 fois plus rapide que le curseur sur mon ordinateur de travail pour 100000 lignes, et c'est aussi une requête en ligne.
Donc, le voici (je suppose qu'il y a une colonne ord dans la table et c'est un numéro séquentiel sans lacunes, pour un traitement rapide là aussi devrait être une contrainte unique sur ce nombre):

;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 violon démo

Mettre à jour J'étais aussi curieux de connaître cette mise à jour avec la variable ou mise à jour excentrique. Donc, habituellement, cela fonctionne bien, mais comment pouvons-nous être sûrs que cela fonctionne à chaque fois? Eh bien, voici un petit truc (trouvé ici - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258) - vous vérifiez simplement ord actuel et précédent et utilisez 1/0 affectation au cas où ils sont différents de ce que vous attendez:

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

D'après ce que j'ai vu si vous avez un index/clé primaire en cluster approprié sur votre table (dans notre cas, il serait index par ord_id) la mise à jour se déroulera de manière linéaire tout le temps (jamais rencontré diviser par zéro). Cela dit, c'est à vous de décider si vous voulez l'utiliser dans le code de production:)

36
répondu Roman Pekar 2014-11-14 10:16:51

L'opérateur APPLY dans SQL 2005 et supérieur fonctionne pour ceci:

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
26
répondu Mike Forman 2009-06-05 18:04:08
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

Vous pouvez également utiliser la fonction ROW_NUMBER () et une table temporaire pour créer une colonne arbitraire à utiliser dans la comparaison sur L'instruction SELECT interne.

10
répondu Sam Axe 2009-05-14 00:02:02

Utilise une sous-requête corrélée. Très simple, voilà:

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

Le code pourrait ne pas être exactement correct, mais je suis sûr que l'idée est.

Le GROUP BY est dans le cas où une date apparaît plus d'une fois, vous ne voudriez la voir qu'une seule fois dans le jeu de résultats.

Si cela ne vous dérange pas de voir des dates répétées, ou si vous voulez voir la valeur et l'id d'origine, alors ce qui suit est ce que vous voulez:

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate
5
répondu KthProg 2014-09-12 20:49:11

Vous pouvez également dénormaliser-stocker les totaux en cours d'exécution dans la même table:

Http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/denormalizing-to-enforce-business-rules-running-totals.aspx

Les sélections fonctionnent beaucoup plus rapidement que toutes les autres solutions, mais les modifications peuvent être plus lentes

4
répondu A-K 2009-06-05 18:14:17

En supposant que le fenêtrage fonctionne sur SQL Server 2008 comme il le fait ailleurs (que j'ai essayé), essayez ceci:

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

MSDN dit qu'il est disponible dans SQL Server 2008 (et peut-être 2005 aussi?) mais je n'ai pas d'instance à portée de main pour l'essayer.

EDIT: Eh bien, apparemment SQL Server n'autorise pas une spécification de fenêtre ("OVER (...) ") sans spécifier "PARTITION BY" (divisant le résultat en groupes mais ne pas agréger de la même manière que GROUP BY). Ennuyeux -- le La référence de syntaxe MSDN suggère que c'est facultatif, mais je n'ai que des instances SqlServer 2000 pour le moment.

La requête que j'ai donnée fonctionne à la fois dans Oracle 10.2.0.3.0 et PostgreSQL 8.4-beta. Alors dites à MS de rattraper son retard;)

3
répondu araqnid 2009-05-14 13:09:05

Ce qui suit produira les résultats requis.

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

Avoir un index cluster sur SomeDate améliorera grandement les performances.

2
répondu Dave Barker 2011-12-09 16:53:54

Si vous utilisez Sql server 2008 R2 ci-dessus. Ensuite, ce serait le moyen le plus court de faire;

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

GAL est utilisé pour obtenir la ligne précédente valeur. Vous pouvez faire google pour plus d'informations.

[1]:

2
répondu shambhu yadav 2017-10-23 06:31:00

Je crois qu'un total en cours d'exécution peut être atteint en utilisant l'opération de jointure interne simple ci-dessous.

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
1
répondu clevster 2011-02-04 00:55:02

Utilisation de jointure Une autre variante consiste à utiliser la rejoindre. Maintenant, la requête pourrait ressembler à:

    SELECT a.id, a.value, SUM(b.Value)FROM   RunTotalTestData a,
    RunTotalTestData b
    WHERE b.id <= a.id
    GROUP BY a.id, a.value 
    ORDER BY a.id;

Pour plus vous pouvez visiter ce lien http://askme.indianyouth.info/details/calculating-simple-running-totals-in-sql-server-12

1
répondu Harikesh Yadav 2016-08-31 04:34:36

Bien que le meilleur moyen de le faire soit d'utiliser une fonction de fenêtre, il peut également être fait en utilisant une simple sous-requête corrélée .

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;
1
répondu Krahul3 2018-02-25 12:54:47
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
0
répondu Mansoor 2016-11-04 07:17:07