web-dev-qa-db-fra.com

Calculer le total/solde courant

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. 

49
Pritesh

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. 


INNER JOIN - ne faites pas ceci:

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.


Sous-requête corrélée - ne le faites pas non plus:

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;

Mise à jour originale - faites-le à vos risques et périls:

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;

CTE récursifs

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).


Le curseur

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;

SQL Server 2012 ou supérieur

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;

CLR

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/


Conclusion

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:

  1. SUM() OVER() ... ROWS, si en 2012 ou plus
  2. Méthode CLR, si possible
  3. Première méthode CTE récursive, si possible
  4. Le curseur
  5. Les autres méthodes CTE récursives
  6. Mise à jour décalée
  7. Jointure et/ou sous-requête corrélée

Pour 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

Totaux cumulés avec Group By

140
Aaron Bertrand

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
5
Madhivanan

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.

1
DanJ
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
0
Abhishek Kanrar

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
0
Ardalan Shahgholi