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

Le partitionnement des résultats dans une requête de totaux cumulés

Si vous n'avez pas besoin de STOCKER les données (ce que vous ne devriez pas, car vous devez mettre à jour les totaux cumulés chaque fois qu'une ligne est modifiée, ajoutée ou supprimée), et si vous ne faites pas confiance à la mise à jour originale (que vous ne devrait pas, car son fonctionnement n'est pas garanti et son comportement peut changer avec un correctif, un service pack, une mise à niveau ou même un index sous-jacent ou un changement de statistiques), vous pouvez essayer ce type de requête au moment de l'exécution. C'est une méthode que son collègue MVP Hugo Kornelis a inventée "itération basée sur un ensemble" (il a posté quelque chose de similaire dans l'un de ses chapitres de Présentations approfondies du MVP SQL Server ). Étant donné que les totaux cumulés nécessitent généralement un curseur sur l'ensemble de l'ensemble, une mise à jour originale sur l'ensemble de l'ensemble ou une seule auto-jointure non linéaire qui devient de plus en plus coûteuse à mesure que le nombre de lignes augmente, l'astuce ici consiste à parcourir une boucle finie. élément dans l'ensemble (dans ce cas, le "rang" de chaque ligne en termes de mois, pour chaque utilisateur - et vous ne traitez chaque rang qu'une seule fois pour toutes les combinaisons utilisateur/mois à ce rang, donc au lieu de parcourir 200 000 lignes, vous bouclez jusqu'à 24 fois).

DECLARE @t TABLE
(
  [user_id] INT, 
  [month] TINYINT,
  total DECIMAL(10,1), 
  RunningTotal DECIMAL(10,1), 
  Rnk INT
);

INSERT @t SELECT [user_id], [month], total, total, 
  RANK() OVER (PARTITION BY [user_id] ORDER BY [month]) 
  FROM dbo.my_table;

DECLARE @rnk INT = 1, @rc INT = 1;

WHILE @rc > 0
BEGIN
  SET @rnk += 1;

  UPDATE c SET RunningTotal = p.RunningTotal + c.total
    FROM @t AS c INNER JOIN @t AS p
    ON c.[user_id] = p.[user_id]
    AND p.rnk = @rnk - 1
    AND c.rnk = @rnk;

  SET @rc = @@ROWCOUNT;
END

SELECT [user_id], [month], total, RunningTotal
FROM @t
ORDER BY [user_id], rnk;

Résultats :

user_id  month   total   RunningTotal
-------  -----   -----   ------------
1        1       2.0     2.0
1        2       1.0     3.0
1        3       3.5     6.5 -- I think your calculation is off
2        1       0.5     0.5
2        2       1.5     2.0
2        3       2.0     4.0

Bien sûr, vous pouvez mettre à jour la table de base à partir de cette variable de table, mais pourquoi s'embêter, puisque ces valeurs stockées ne sont bonnes que jusqu'à la prochaine fois que la table est touchée par une instruction DML ?

UPDATE mt
  SET cumulative_total = t.RunningTotal
  FROM dbo.my_table AS mt
  INNER JOIN @t AS t
  ON mt.[user_id] = t.[user_id]
  AND mt.[month] = t.[month];

Étant donné que nous ne nous appuyons sur aucun type de commande implicite, cela est pris en charge à 100% et mérite une comparaison des performances par rapport à la mise à jour originale non prise en charge. Même s'il ne le bat pas mais s'en rapproche, vous devriez quand même envisager de l'utiliser à mon humble avis.

Quant à la solution SQL Server 2012, Matt mentionne RANGE mais comme cette méthode utilise un spool sur disque, vous devez également tester avec ROWS au lieu de simplement courir avec RANGE . Voici un exemple rapide pour votre cas :

SELECT
  [user_id],
  [month],
  total,
  RunningTotal = SUM(total) OVER 
  (
    PARTITION BY [user_id] 
    ORDER BY [month] ROWS UNBOUNDED PRECEDING
  )
FROM dbo.my_table
ORDER BY [user_id], [month];

Comparez cela avec RANGE UNBOUNDED PRECEDING ou pas de ROWS\RANGE du tout (qui utilisera également le RANGE bobine sur disque). Ce qui précède aura une durée globale et un moyen inférieurs moins d'E/S, même si le plan semble légèrement plus complexe (un opérateur de projet de séquence supplémentaire).

J'ai récemment publié un article de blog décrivant certaines différences de performances que j'ai observées pour un scénario de totaux cumulés spécifique :

http://www.sqlperformance.com/2012/07 /t-sql-queries/cumul-totaux