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
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
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
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 :)
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
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.
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
Vous pouvez également dénormaliser - stocker les totaux cumulés dans le même tableau:
Sélectionne le travail beaucoup plus rapidement que toute autre solution, mais les modifications peuvent être plus lentes
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;)
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]:
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;
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
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
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.
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
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