Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Calculer le total cumulé / le solde cumulé

Pour ceux qui n'utilisent pas SQL Server 2012 ou supérieur, un curseur est probablement le plus efficace supporté et garanti méthode en dehors du CLR. Il existe d'autres approches telles que la "mise à jour originale" qui peut être légèrement plus rapide mais dont le fonctionnement n'est pas garanti à l'avenir, et bien sûr des approches basées sur des ensembles avec des profils de performances hyperboliques à mesure que la table s'agrandit, et des méthodes CTE récursives qui nécessitent souvent des #tempdb I/O ou entraîner des débordements qui produisent à peu près le même impact.

INNER JOIN - ne pas faire 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? Au fur et à mesure que la table s'agrandit, chaque ligne incrémentielle nécessite la lecture de n-1 lignes dans la table. Ceci est exponentiel et lié aux échecs, aux délais d'attente ou simplement aux utilisateurs en colère.

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

Le formulaire de sous-requête est tout aussi douloureux pour des raisons tout aussi douloureuses.

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 de "mise à jour originale" est plus efficace que celle ci-dessus, mais le comportement n'est pas documenté, il n'y a aucune garantie concernant l'ordre et le comportement peut fonctionner aujourd'hui mais pourrait échouer à 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 principale raison pour laquelle j'ai même répondu à cette question au lieu de la fermer en tant que doublon est que l'autre question a une mise à jour bizarre 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, sans espace :

;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 vous fier à 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 que nous ne connaissons pas), vous pouvez obtenir de meilleures performances globales en remplissant d'abord les colonnes pertinentes uniquement dans une table #temp, et en les traitant au lieu 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 rivalisant avec la mise à jour originale, mais elle fait une grande hypothèse sur la nature des données (pas de lacunes). Les deux autres méthodes se replieront et dans ces cas, vous pouvez également utiliser un curseur (si vous ne pouvez pas utiliser CLR et que vous n'êtes pas encore sur SQL Server 2012 ou supérieur).

Curseur

On dit à tout le monde que les curseurs sont mauvais et qu'ils doivent être évités à tout prix, mais cela dépasse en fait 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 du 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 elle fonctionne également mieux 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 sur des ensembles de données plus volumineux, vous constaterez que ce qui précède fonctionne bien mieux que l'une des deux options suivantes, car RANGE utilise un spool sur disque (et la valeur par défaut utilise RANGE). Cependant, il est également important de noter que le comportement et les résultats peuvent différer, alors assurez-vous qu'ils renvoient tous les deux des résultats corrects avant de décider 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 sur 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 êtes sur SQL Server 2012 ou supérieur, le choix est évident - utilisez le nouveau SUM() OVER() construction (avec ROWS vs RANGE ). Pour les versions antérieures, vous souhaiterez comparer les performances des approches alternatives sur votre schéma, vos données et, en tenant compte des facteurs non liés aux performances, déterminer quelle approche vous convient le mieux. Il peut très bien s'agir de 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. Curseur
  5. Les autres méthodes CTE récursives
  6. Mise à jour originale
  7. Joindre et/ou sous-requête corrélée

Pour plus d'informations sur les comparaisons de performances de ces méthodes, consultez cette question sur http://dba.stackexchange.com :

https://dba.stackexchange.com/questions/19507/running-total-with-count

J'ai également ajouté plus de détails sur ces comparaisons ici :

http://www.sqlperformance.com/2012/07/t-sql-queries/running-totals

Pour les totaux cumulés groupés/partitionnés, consultez également les articles suivants :

http://sqlperformance.com/2014/01/t-sql-queries/grouped-running-totals

Le partitionnement donne une requête de totaux cumulés

Totaux cumulés multiples avec grouper par