web-dev-qa-db-fra.com

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 retourne un total en ordre chronologique, 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 type de méthode qui utilise l’astuce d’agrégation-ensemble-instruction:

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 car vous ne pouvez pas nécessairement garantir que l'instruction UPDATE traitera les lignes dans le bon ordre. Peut-être pouvons-nous obtenir des réponses définitives à ce problème.

Mais peut-être y at-il d’autres manières que les gens peuvent suggérer?

edit: Maintenant avec un SqlFiddle avec la configuration et l'exemple 'update trick' ci-dessus

156
codeulike

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 d'effectuer les tâches suivantes:

 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 des rares cas où le curseur est le plus rapide, bien que je doive effectuer des analyses comparatives sur de gros résultats.

L’astuce de mise à jour est pratique mais j’estime qu’elle est 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 clé primaire croissante, vous serez probably en sécurité. Mais vous vous appuyez sur un détail d'implémentation de SQL Server non documenté (même si la requête est exécutée par deux processus, je me demande ce qui va se passer, voir: MAXDOP):

Échantillon de travail complet:

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 point de repère, c'est ce qu'il y a de plus grave.

Le moyen le plus rapide de SAFE pour ce faire serait le curseur, il s'agit d'un ordre de grandeur plus rapide que la sous-requête corrélée de jointure croisée.

Le moyen le plus rapide est l’astuce UPDATE. Mon seul souci est que je ne suis pas sûr 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.

En bout de ligne, pour le code de production, je voudrais aller avec le curseur.

Données de 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
122
Sam Saffron

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

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

SQL Fiddle

110
Mikael Eriksson

Bien que Sam Saffron ait fait de l'excellent travail à ce sujet, il n'a toujours pas fourni de code d'expression de table commune récursive pour résoudre ce problème. Et pour nous qui travaillons avec SQL Server 2008 R2 et non pas Denali, c'est toujours le moyen le plus rapide de fonctionner, il est environ 10 fois plus rapide que le curseur sur mon ordinateur de travail pour 100 000 lignes, et c'est aussi une requête en ligne.
Donc, le voici (je suppose qu'il y a une colonne ord dans le tableau et son numéro séquentiel sans espaces, pour un traitement rapide, une contrainte unique sur ce nombre devrait également exister):

;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 J'étais aussi curieux à propos de cette mise à jour avec variable ou Mise à jour originale . Donc, en général, ça marche, 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 venez de vérifier le _ actuel et précédent ord et utilisez 1/0 affectation au cas où elles seraient différentes de ce que vous attendiez:

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 une clé d'index/clé primaire en cluster appropriée sur votre table (dans notre cas, il s'agirait d'un index de ord_id), la mise à jour se déroulera de manière linéaire tout le temps (jamais rencontré de division par zéro) . Cela dit, c'est à vous de décider si vous voulez l'utiliser dans le code de production :)

40
Roman Pekar

L’opérateur APPLY dans SQL 2005 et versions ultérieures fonctionne pour cela:

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
28
Mike Forman
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 de l'instruction SELECT interne.

11
Sam Axe

Utilisez une sous-requête corrélée. Très simple, c'est parti:

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 n'est peut-être pas tout à fait correct, mais je suis sûr que l'idée est.

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

Si cela ne vous dérange pas de voir les dates se répéter, ou si vous voulez voir la valeur et l'id d'origine, voici 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
7
KthProg

Vous pouvez également dénormaliser - stocker les totaux cumulés dans le même tableau:

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

Sélectionne le travail beaucoup plus rapidement que toute autre solution, mais les modifications peuvent être plus lentes

5
A-K

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

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

MSDN indique qu'il est disponible dans SQL Server 2008 (et peut-être aussi en 2005?), mais je n'ai pas d'instance sous la main pour l'essayer.

EDIT: eh bien, apparemment, SQL Server ne permet pas de spécifier une fenêtre ("OVER (...)") sans spécifier "PARTITION BY" (diviser le résultat en groupes, mais ne pas l'agréger comme le fait GROUP BY). Ennuyeux - la référence de syntaxe MSDN suggère que c'est optionnel, mais je n'ai que des instances de 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 se rattraper;)

4
araqnid

Si vous utilisez Sql Server 2008 R2 ci-dessus. Alors, ce serait le chemin le plus court;

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

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

[1]:

4
shambhu yadav

Bien que la meilleure solution consiste à utiliser une fonction de fenêtre, vous pouvez également utiliser 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;
2
Krahul3

Utiliser join Une autre variante consiste à utiliser join. 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

2
Harikesh Yadav

Je pense qu'un total cumulé peut être obtenu en utilisant la simple opération INNER JOIN 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
2
clevster

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 clusterisé sur SomeDate améliorera considérablement les performances.

2
Dave Barker
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
Mansoor

Voici 2 façons simples de calculer le total cumulé:

Approche 1 : Il peut être écrit de cette façon si votre SGBD prend en charge les fonctions analytiques.

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

Approche 2 : Vous pouvez utiliser OUTER APPLY si votre version de base de données/SGBD lui-même ne prend pas en charge les fonctions analytiques.

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;

Remarque: - Si vous devez calculer séparément le total cumulé pour différentes partitions, vous pouvez le faire comme indiqué ici: Calcul des totaux cumulés sur plusieurs lignes et regroupement par ID

0
san