J'ai une table:
create table Transactions(Tid int,amt int)
Avec 5 rangées:
insert into Transactions values(1, 100)
insert into Transactions values(2, -50)
insert into Transactions values(3, 100)
insert into Transactions values(4, -100)
insert into Transactions values(5, 200)
Sortie désirée:
TID amt balance
--- ----- -------
1 100 100
2 -50 50
3 100 150
4 -100 50
5 200 250
Fondamentalement, pour le premier enregistrement, le solde sera le même que amt
, le deuxième solde sera l'addition du solde précédent + actuel amt
. Je cherche une approche optimale. Je pouvais penser à utiliser une fonction ou une sous-requête corrélée mais je ne savais pas exactement comment le faire.
Pour ceux qui n'utilisent pas SQL Server 2012 ou une version ultérieure, un curseur est probablement la méthode la plus efficace supportée et garantie en dehors de CLR. Il existe d’autres approches telles que la "mise à jour originale" qui peuvent être légèrement plus rapides mais ne sont pas sûres de fonctionner à l’avenir, et bien sûr des approches basées sur les ensembles avec des profils de performances hyperboliques à mesure que le tableau s’agrandit et des méthodes de CTE récursives nécessitant souvent une analyse directe. #tempdb I/O ou provoquer des déversements produisant à peu près le même impact.
L’approche lente, basée sur les ensembles, est de la forme:
SELECT t1.TID, t1.amt, RunningTotal = SUM(t2.amt)
FROM dbo.Transactions AS t1
INNER JOIN dbo.Transactions AS t2
ON t1.TID >= t2.TID
GROUP BY t1.TID, t1.amt
ORDER BY t1.TID;
La raison pour laquelle c'est lent? À mesure que la table s'agrandit, chaque ligne incrémentielle nécessite la lecture de n-1 lignes dans la table. C'est exponentiel et lié aux échecs, aux délais d'attente ou aux utilisateurs en colère.
La forme de sous-requête est également douloureuse pour des raisons similaires.
SELECT TID, amt, RunningTotal = amt + COALESCE(
(
SELECT SUM(amt)
FROM dbo.Transactions AS i
WHERE i.TID < o.TID), 0
)
FROM dbo.Transactions AS o
ORDER BY TID;
La méthode "mise à jour décalée" est plus efficace que la méthode ci-dessus, mais le comportement n'est pas documenté, il n'y a aucune garantie d'ordre, et le comportement pourrait fonctionner aujourd'hui, mais pourrait se rompre à l'avenir. J'inclus ceci parce que c'est une méthode populaire et efficace, mais cela ne signifie pas que je l'approuve. La raison principale pour laquelle j'ai même répondu à cette question au lieu de la fermer en double est que l'autre question a une mise à jour originale comme réponse acceptée .
DECLARE @t TABLE
(
TID INT PRIMARY KEY,
amt INT,
RunningTotal INT
);
DECLARE @RunningTotal INT = 0;
INSERT @t(TID, amt, RunningTotal)
SELECT TID, amt, RunningTotal = 0
FROM dbo.Transactions
ORDER BY TID;
UPDATE @t
SET @RunningTotal = RunningTotal = @RunningTotal + amt
FROM @t;
SELECT TID, amt, RunningTotal
FROM @t
ORDER BY TID;
Ce premier s'appuie sur TID pour être contigu, pas de lacunes:
;WITH x AS
(
SELECT TID, amt, RunningTotal = amt
FROM dbo.Transactions
WHERE TID = 1
UNION ALL
SELECT y.TID, y.amt, x.RunningTotal + y.amt
FROM x
INNER JOIN dbo.Transactions AS y
ON y.TID = x.TID + 1
)
SELECT TID, amt, RunningTotal
FROM x
ORDER BY TID
OPTION (MAXRECURSION 10000);
Si vous ne pouvez pas compter sur cela, vous pouvez utiliser cette variante, qui construit simplement une séquence contiguë à l'aide de ROW_NUMBER()
:
;WITH y AS
(
SELECT TID, amt, rn = ROW_NUMBER() OVER (ORDER BY TID)
FROM dbo.Transactions
), x AS
(
SELECT TID, rn, amt, rt = amt
FROM y
WHERE rn = 1
UNION ALL
SELECT y.TID, y.rn, y.amt, x.rt + y.amt
FROM x INNER JOIN y
ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
FROM x
ORDER BY x.rn
OPTION (MAXRECURSION 10000);
En fonction de la taille des données (par exemple, des colonnes inconnues), vous pouvez améliorer les performances globales en remplissant d'abord les colonnes appropriées dans une table #temp, puis en les traitant à la place de la table de base:
CREATE TABLE #x
(
rn INT PRIMARY KEY,
TID INT,
amt INT
);
INSERT INTO #x (rn, TID, amt)
SELECT ROW_NUMBER() OVER (ORDER BY TID),
TID, amt
FROM dbo.Transactions;
;WITH x AS
(
SELECT TID, rn, amt, rt = amt
FROM #x
WHERE rn = 1
UNION ALL
SELECT y.TID, y.rn, y.amt, x.rt + y.amt
FROM x INNER JOIN #x AS y
ON y.rn = x.rn + 1
)
SELECT TID, amt, RunningTotal = rt
FROM x
ORDER BY TID
OPTION (MAXRECURSION 10000);
DROP TABLE #x;
Seule la première méthode CTE fournira des performances équivalentes à la mise à jour originale, mais elle repose sur une hypothèse de taille quant à la nature des données (pas de lacunes). Les deux autres méthodes reculeront et dans ces cas, vous pourrez également utiliser un curseur (si vous ne pouvez pas utiliser CLR et que vous n'êtes pas encore sur SQL Server 2012 ou une version ultérieure).
On dit à tout le monde que les curseurs sont diaboliques et qu’ils devraient être évités à tout prix, mais cela surpasse en réalité les performances de la plupart des autres méthodes prises en charge et est plus sûr que la mise à jour originale. Les seules que je préfère à la solution de curseur sont les méthodes 2012 et CLR (ci-dessous):
CREATE TABLE #x
(
TID INT PRIMARY KEY,
amt INT,
rt INT
);
INSERT #x(TID, amt)
SELECT TID, amt
FROM dbo.Transactions
ORDER BY TID;
DECLARE @rt INT, @tid INT, @amt INT;
SET @rt = 0;
DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR SELECT TID, amt FROM #x ORDER BY TID;
OPEN c;
FETCH c INTO @tid, @amt;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @rt = @rt + @amt;
UPDATE #x SET rt = @rt WHERE TID = @tid;
FETCH c INTO @tid, @amt;
END
CLOSE c; DEALLOCATE c;
SELECT TID, amt, RunningTotal = rt
FROM #x
ORDER BY TID;
DROP TABLE #x;
Les nouvelles fonctions de fenêtre introduites dans SQL Server 2012 rendent cette tâche beaucoup plus facile (et ses performances sont également meilleures que toutes les méthodes ci-dessus):
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID ROWS UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
Notez que pour les plus grands ensembles de données, vous constaterez que les opérations décrites ci-dessus fonctionnent beaucoup mieux que l'une des deux options suivantes, car RANGE utilise un spool sur disque (et le paramètre par défaut utilise RANGE). Cependant, il est également important de noter que le comportement et les résultats peuvent différer. Veillez donc à ce qu'ils retournent tous les deux des résultats corrects avant de choisir entre eux en fonction de cette différence.
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID)
FROM dbo.Transactions
ORDER BY TID;
SELECT TID, amt,
RunningTotal = SUM(amt) OVER (ORDER BY TID RANGE UNBOUNDED PRECEDING)
FROM dbo.Transactions
ORDER BY TID;
Pour être complet, je propose un lien vers la méthode CLR de Pavel Pawlowski, qui est de loin la méthode préférable pour les versions antérieures à SQL Server 2012 (mais pas 2000 évidemment).
http://www.pawlowski.cz/2010/09/sql-server-and-fastest-running-totals-using-clr/
Si vous utilisez SQL Server 2012 ou une version ultérieure, le choix est évident: utilisez la nouvelle construction SUM() OVER()
(avec ROWS
contre RANGE
). Pour les versions antérieures, vous souhaiterez comparer les performances des différentes approches de votre schéma, de vos données et - en tenant compte des facteurs non liés à la performance - pour déterminer quelle approche vous convient. C'est très bien l'approche CLR. Voici mes recommandations, par ordre de préférence:
SUM() OVER() ... ROWS
, si en 2012 ou plusPour plus d'informations sur les comparaisons de performances de ces méthodes, consultez cette question à http://dba.stackexchange.com :
https://dba.stackexchange.com/questions/19507/running-total-with-count
J'ai également blogué plus de détails sur ces comparaisons ici:
http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals
Également pour les totaux cumulés/partitionnés, voir les articles suivants:
http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals
Le partitionnement aboutit à une requête de totaux en cours d'exécution
Si vous utilisez la version 2012, voici une solution
select *, sum(amt) over (order by Tid) as running_total from Transactions
Pour les versions antérieures
select *,(select sum(amt) from Transactions where Tid<=t.Tid) as running_total from Transactions as t
Nous sommes sur 2008R2 et j'utilise des variables et une table temporaire. Cela vous permet également de personnaliser le calcul de chaque ligne à l'aide d'une instruction de casse (c'est-à-dire que certaines transactions peuvent agir différemment ou vous pouvez uniquement vouloir un total pour des types de transaction spécifiques).
DECLARE @RunningBalance int = 0
SELECT Tid, Amt, 0 AS RunningBalance
INTO #TxnTable
FROM Transactions
ORDER BY Tid
UPDATE #TxnTable
SET @RunningBalance = RunningBalance = @RunningBalance + Amt
SELECT * FROM #TxnTable
DROP TABLE #TxnTable
Nous avons une table de transactions avec 2,3 millions de lignes avec un élément contenant plus de 3 300 transactions et l'exécution de ce type de requête ne prend pas de temps.
select v.ID
,CONVERT(VARCHAR(10), v.EntryDate, 103) + ' ' + convert(VARCHAR(8), v.EntryDate, 14)
as EntryDate
,case
when v.CreditAmount<0
then
ISNULL(v.CreditAmount,0)
else
0
End as credit
,case
when v.CreditAmount>0
then
v.CreditAmount
else
0
End as debit
,Balance = SUM(v.CreditAmount) OVER (ORDER BY v.ID ROWS UNBOUNDED PRECEDING)
from VendorCredit v
order by v.EntryDate desc
Dans SQL Server 2008+
SELECT T1.* ,
T2.RunningSum
FROM dbo.Transactions As T1
CROSS APPLY ( SELECT SUM(amt) AS RunningSum
FROM dbo.Transactions AS CAT1
WHERE ( CAT1.TId <= T1.TId )
) AS T2
Dans SQL Server 2012+
SELECT * ,
SUM(T1.amt) OVER ( ORDER BY T1.TId
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS RunningTotal
FROM dbo.Transactions AS t1