Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Die Partitionierung führt zu einer laufenden Summenabfrage

Wenn Sie die Daten nicht SPEICHERN müssen (was Sie nicht tun sollten, da Sie die laufenden Summen jedes Mal aktualisieren müssen, wenn eine Zeile geändert, hinzugefügt oder gelöscht wird) und wenn Sie dem skurrilen Update (dem Sie sollte nicht, da es nicht garantiert funktioniert und sein Verhalten sich mit einem Hotfix, Service Pack, Upgrade oder sogar einer zugrunde liegenden Index- oder Statistikänderung ändern könnte), können Sie diese Art von Abfrage zur Laufzeit ausprobieren. Dies ist eine Methode, die MVP-Kollege Hugo Kornelis als „satzbasierte Iteration“ bezeichnete (er hat etwas Ähnliches in einem seiner Kapitel von gepostet). SQL Server MVP Deep Dives ). Da laufende Summen normalerweise einen Cursor über dem gesamten Satz, eine skurrile Aktualisierung über den gesamten Satz oder einen einzelnen nichtlinearen Self-Join erfordern, der mit zunehmender Zeilenanzahl immer teurer wird, besteht der Trick hier darin, einige endliche zu durchlaufen Element in der Menge (in diesem Fall der "Rang" jeder Zeile in Bezug auf den Monat für jeden Benutzer - und Sie verarbeiten jeden Rang nur einmal für alle Benutzer/Monat-Kombinationen auf diesem Rang, also anstatt 200.000 Zeilen zu durchlaufen, Sie Schleifen bis zu 24 Mal).

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;

Ergebnisse:

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

Natürlich können Sie Aktualisieren Sie die Basistabelle aus dieser Tabellenvariablen, aber warum sollten Sie sich die Mühe machen, da diese gespeicherten Werte nur gültig sind, bis die Tabelle das nächste Mal von einer DML-Anweisung berührt wird?

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];

Da wir uns nicht auf irgendeine implizite Reihenfolge verlassen, wird dies zu 100 % unterstützt und verdient einen Leistungsvergleich mit dem nicht unterstützten schrulligen Update. Auch wenn es nicht schlägt, aber nahe kommt, sollten Sie IMHO trotzdem in Betracht ziehen, es zu verwenden.

Bezüglich der SQL Server 2012-Lösung erwähnt Matt RANGE Da diese Methode jedoch einen Spool auf der Festplatte verwendet, sollten Sie auch mit ROWS testen anstatt einfach mit RANGE zu laufen . Hier ist ein kurzes Beispiel für Ihren Fall:

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];

Vergleichen Sie dies mit RANGE UNBOUNDED PRECEDING oder kein ROWS\RANGE überhaupt (was auch den RANGE verwendet). Spool auf der Festplatte). Die oben genannten haben eine geringere Gesamtdauer und Weg weniger I/O, obwohl der Plan etwas komplexer aussieht (ein zusätzlicher Sequenz-Projektoperator).

Ich habe kürzlich einen Blogpost veröffentlicht, in dem einige Leistungsunterschiede beschrieben werden, die ich für ein bestimmtes Szenario mit laufenden Summen beobachtet habe:

http://www.sqlperformance.com/2012/07 /t-sql-queries/laufende-summen