Laufende Summe basierend auf einer anderen Spalte zurücksetzen


10

Ich versuche die laufende Summe zu berechnen. Es sollte jedoch zurückgesetzt werden, wenn die kumulative Summe größer als ein anderer Spaltenwert ist

create table #reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
)

insert into #reset_runn_total
values 
(1,10,1),
(8,12,1),(6,14,1),(5,10,1),(6,13,1),(3,11,1),(9,8,1),(10,12,1)


SELECT Row_number()OVER(partition BY grp ORDER BY id)AS rn,*
INTO   #test
FROM   #reset_runn_total

Indexdetails:

CREATE UNIQUE CLUSTERED INDEX ix_load_reset_runn_total
  ON #test(rn, grp) 

Beispieldaten

+----+-----+-----------+-----+
| id | val | reset_val | Grp |
+----+-----+-----------+-----+
|  1 |   1 |        10 | 1   |
|  2 |   8 |        12 | 1   |
|  3 |   6 |        14 | 1   |
|  4 |   5 |        10 | 1   |
|  5 |   6 |        13 | 1   |
|  6 |   3 |        11 | 1   |
|  7 |   9 |         8 | 1   |
|  8 |  10 |        12 | 1   |
+----+-----+-----------+-----+ 

Erwartetes Ergebnis

+----+-----+-----------------+-------------+
| id | val |    reset_val    | Running_tot |
+----+-----+-----------------+-------------+
|  1 |   1 | 10              |       1     |  
|  2 |   8 | 12              |       9     |  --1+8
|  3 |   6 | 14              |       15    |  --1+8+6 -- greater than reset val
|  4 |   5 | 10              |       5     |  --reset 
|  5 |   6 | 13              |       11    |  --5+6
|  6 |   3 | 11              |       14    |  --5+6+3 -- greater than reset val
|  7 |   9 | 8               |       9     |  --reset -- greater than reset val 
|  8 |  10 | 12              |      10     |  --reset
+----+-----+-----------------+-------------+

Abfrage:

Ich habe das Ergebnis mit Recursive CTE. Die ursprüngliche Frage finden Sie hier /programming/42085404/reset-running-total-based-on-another-column

;WITH cte
     AS (SELECT rn,id,
                val,
                reset_val,
                grp,
                val                   AS running_total,
                Iif (val > reset_val, 1, 0) AS flag
         FROM   #test
         WHERE  rn = 1
         UNION ALL
         SELECT r.*,
                Iif(c.flag = 1, r.val, c.running_total + r.val),
                Iif(Iif(c.flag = 1, r.val, c.running_total + r.val) > r.reset_val, 1, 0)
         FROM   cte c
                JOIN #test r
                  ON r.grp = c.grp
                     AND r.rn = c.rn + 1)
SELECT *
FROM   cte 

Gibt es eine bessere Alternative T-SQLohne CLR.?


Besser wie? Weist diese Abfrage eine schlechte Leistung auf? Mit welchen Metriken?
Aaron Bertrand

@AaronBertrand - Zum besseren Verständnis habe ich Beispieldaten für nur eine Gruppe veröffentlicht. Ich muss dasselbe für 50000Gruppen mit 60 Ausweisen tun . Die Gesamtzahl der Datensätze liegt also bei etwa 3000000. Bin mir sicher Recursive CTEnicht gut skalierbar 3000000. Aktualisiert die Metriken, wenn ich wieder im Büro bin. Können wir dies erreichen, sum()Over(Order by)wie Sie es in diesem Artikel verwendet haben ? Sqlperformance.com/2012/07/t-sql-queries/running-totals
P ரதீப்

Ein Cursor könnte besser sein als ein rekursiver CTE
Paparazzo

Antworten:


6

Ich habe mir ähnliche Probleme angesehen und nie eine Fensterfunktionslösung gefunden, die einen einzigen Durchlauf über die Daten durchführt. Ich denke nicht, dass es möglich ist. Fensterfunktionen müssen auf alle Werte in einer Spalte angewendet werden können. Das macht solche Reset-Berechnungen sehr schwierig, da ein Reset den Wert für alle folgenden Werte ändert.

Eine Möglichkeit, über das Problem nachzudenken, besteht darin, dass Sie das gewünschte Endergebnis erhalten, wenn Sie eine grundlegende laufende Summe berechnen, solange Sie die laufende Summe von der richtigen vorherigen Zeile subtrahieren können. In Ihren Beispieldaten ist der Wert für id4 beispielsweise der running total of row 4 - the running total of row 3. Der Wert für id6 ist der Wert, running total of row 6 - the running total of row 3da noch kein Reset durchgeführt wurde. Der Wert für id7 ist der running total of row 7 - the running total of row 6und so weiter.

Ich würde dies mit T-SQL in einer Schleife angehen. Ich wurde ein wenig mitgerissen und denke, ich habe eine vollständige Lösung. Für 3 Millionen Zeilen und 500 Gruppen wurde der Code auf meinem Desktop in 24 Sekunden fertiggestellt. Ich teste mit SQL Server 2016 Developer Edition mit 6 vCPU. Ich nutze parallele Einfügungen und parallele Ausführung im Allgemeinen, sodass Sie möglicherweise den Code ändern müssen, wenn Sie eine ältere Version verwenden oder DOP-Einschränkungen haben.

Unter dem Code, mit dem ich die Daten generiert habe. Die Bereiche auf VALund RESET_VALsollten Ihren Beispieldaten ähnlich sein.

drop table if exists reset_runn_total;

create table reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
);

DECLARE 
@group_num INT,
@row_num INT;
BEGIN
    SET NOCOUNT ON;
    BEGIN TRANSACTION;

    SET @group_num = 1;
    WHILE @group_num <= 50000 
    BEGIN
        SET @row_num = 1;
        WHILE @row_num <= 60
        BEGIN
            INSERT INTO reset_runn_total WITH (TABLOCK)
            SELECT 1 + ABS(CHECKSUM(NewId())) % 10, 8 + ABS(CHECKSUM(NewId())) % 8, @group_num;

            SET @row_num = @row_num + 1;
        END;
        SET @group_num = @group_num + 1;
    END;
    COMMIT TRANSACTION;
END;

Der Algorithmus ist wie folgt:

1) Fügen Sie zunächst alle Zeilen mit einer Standardlaufsumme in eine temporäre Tabelle ein.

2) In einer Schleife:

2a) Berechnen Sie für jede Gruppe die erste Zeile mit einer laufenden Summe über dem in der Tabelle verbleibenden reset_value und speichern Sie die ID, die zu große laufende Summe und die zu große laufende Summe in einer temporären Tabelle.

2b) Löschen Sie Zeilen aus der ersten temporären Tabelle in eine temporäre Ergebnistabelle, die IDkleiner oder gleich der IDin der zweiten temporären Tabelle ist. Verwenden Sie die anderen Spalten, um die laufende Summe nach Bedarf anzupassen.

3) Nachdem der Löschvorgang keine Zeilen mehr verarbeitet, führen Sie eine zusätzliche DELETE OUTPUTin die Ergebnistabelle ein. Dies gilt für Zeilen am Ende der Gruppe, die den Rücksetzwert niemals überschreiten.

Ich werde Schritt für Schritt eine Implementierung des obigen Algorithmus in T-SQL durchgehen.

Erstellen Sie zunächst einige temporäre Tabellen. #initial_resultsenthält die Originaldaten mit der Standardlaufsumme, #group_bookkeepingwird in jeder Schleife aktualisiert, um herauszufinden, welche Zeilen verschoben werden können, und #final_resultsenthält die Ergebnisse, wobei die Laufsumme für Zurücksetzungen angepasst ist.

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

Ich erstelle den Clustered-Index für die temporäre Tabelle, damit das Einfügen und das Erstellen des Index parallel erfolgen können. Hat einen großen Unterschied auf meiner Maschine gemacht, aber möglicherweise nicht auf Ihrer. Das Erstellen eines Index für die Quelltabelle schien nicht zu helfen, aber das könnte auf Ihrem Computer helfen.

Der folgende Code wird in der Schleife ausgeführt und aktualisiert die Buchhaltungstabelle. Für jede Gruppe müssen wir das Find-Maximum ermitteln, IDdas in die Ergebnistabelle verschoben werden soll. Wir benötigen die laufende Summe aus dieser Zeile, damit wir sie von der anfänglichen laufenden Summe abziehen können. Die grp_doneSpalte wird auf 1 gesetzt, wenn für a keine Arbeit mehr zu erledigen ist grp.

WITH UPD_CTE AS (
        SELECT 
        #grp_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_update
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        INNER JOIN #initial_results IR ON #group_bookkeeping.grp = ir.grp
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_update
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
OPTION (LOOP JOIN);

Wirklich kein Fan des LOOP JOINHinweises im Allgemeinen, aber dies ist eine einfache Abfrage und es war der schnellste Weg, um das zu bekommen, was ich wollte. Um die Antwortzeit wirklich zu optimieren, wollte ich parallele verschachtelte Schleifenverknüpfungen anstelle von DOP 1-Zusammenführungsverknüpfungen.

Der folgende Code wird in der Schleife ausgeführt und verschiebt Daten aus der Anfangstabelle in die Endergebnis-Tabelle. Beachten Sie die Anpassung an die anfängliche laufende Summe.

DELETE ir
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
WHERE tb.grp_done = 0;

Für Ihre Bequemlichkeit ist unten der vollständige Code:

DECLARE @RC INT;
BEGIN
SET NOCOUNT ON;

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

SET @RC = 1;
WHILE @RC > 0 
BEGIN
    WITH UPD_CTE AS (
        SELECT 
        #group_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_move
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        CROSS APPLY (SELECT ID, RESET_VAL, initial_running_total FROM #initial_results ir WHERE #group_bookkeeping.grp = ir.grp ) ir
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_move
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
    OPTION (LOOP JOIN);

    DELETE ir
    OUTPUT DELETED.id,  
        DELETED.VAL,  
        DELETED.RESET_VAL,  
        DELETED.GRP ,
        DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
    FROM #initial_results ir
    INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
    WHERE tb.grp_done = 0;

    SET @RC = @@ROWCOUNT;
END;

DELETE ir 
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP;

CREATE CLUSTERED INDEX f1 ON #final_results (grp, id);

/* -- do something with the data
SELECT *
FROM #final_results
ORDER BY grp, id;
*/

DROP TABLE #final_results;
DROP TABLE #initial_results;
DROP TABLE #group_bookkeeping;

END;

einfach genial Ich werde dich mit Kopfgeld
belohnen

In unserem Server dauerte Ihre für 50000 grp und 60 id 1 Minute und 10 Sekunden. Recursive CTEdauerte 2 Minuten und 15 Sekunden
P ரதீப்

Ich habe beide Codes mit den gleichen Daten getestet. Deiner war großartig. Kann es weiter verbessert werden?
P ரதீப்

Ich meinte, ich habe Ihren Code auf unseren realen Daten ausgeführt und getestet. Die Berechnung wird in meinem realen Verfahren in temporären Tabellen verarbeitet. Höchstwahrscheinlich sollte sie dicht gepackt sein. Es ist gut, wenn es auf etwa 30 Sekunden reduziert werden kann
P ரதீப்

@Prdp Versuchte einen schnellen Ansatz, der ein Update verwendete, aber es schien schlimmer zu sein. Ich werde mich eine Weile nicht mehr damit befassen können. Versuchen Sie zu protokollieren, wie lange jeder Vorgang dauert, damit Sie herausfinden können, welcher Teil auf Ihrem Server am langsamsten ausgeführt wird. Es ist definitiv möglich, dass es eine Möglichkeit gibt, diesen Code oder einen besseren Algorithmus im Allgemeinen zu beschleunigen.
Joe Obbish

4

Verwenden eines CURSORS:

ALTER TABLE #reset_runn_total ADD RunningTotal int;

DECLARE @id int, @val int, @reset int, @acm int, @grp int, @last_grp int;
SET @acm = 0;

DECLARE curRes CURSOR FAST_FORWARD FOR 
SELECT id, val, reset_val, grp
FROM #reset_runn_total
ORDER BY grp, id;

OPEN curRes;
FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
SET @last_grp = @grp;

WHILE @@FETCH_STATUS = 0  
BEGIN
    IF @grp <> @last_grp SET @acm = 0;
    SET @last_grp = @grp;
    SET @acm = @acm + @val;
    UPDATE #reset_runn_total
    SET RunningTotal = @acm
    WHERE id = @id;
    IF @acm > @reset SET @acm = 0;
    FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
END

CLOSE curRes;
DEALLOCATE curRes;

+----+-----+-----------+-------------+
| id | val | reset_val | RunningTotal|
+----+-----+-----------+-------------+
| 1  | 1   | 10        |     1       |
+----+-----+-----------+-------------+
| 2  | 8   | 12        |     9       |
+----+-----+-----------+-------------+
| 3  | 6   | 14        |     15      |
+----+-----+-----------+-------------+
| 4  | 5   | 10        |     5       |
+----+-----+-----------+-------------+
| 5  | 6   | 13        |     11      |
+----+-----+-----------+-------------+
| 6  | 3   | 11        |     14      |
+----+-----+-----------+-------------+
| 7  | 9   | 8         |     9       |
+----+-----+-----------+-------------+
| 8  | 10  | 12        |     10      |
+----+-----+-----------+-------------+

Überprüfen Sie hier: http://rextester.com/WSPLO95303


3

Nicht mit Fenstern versehen, aber reine SQL-Version:

WITH x AS (
    SELECT TOP 1 id,
           val,
           reset_val,
           val AS running_total,
           1 AS level 
      FROM reset_runn_total
    UNION ALL
    SELECT r.id,
           r.val,
           r.reset_val,
           CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END,
           level = level + 1
      FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
) SELECT
  *
FROM x
WHERE NOT EXISTS (
        SELECT 1
        FROM x AS x2
        WHERE x2.id = x.id
        AND x2.level > x.level
    )
ORDER BY id, level DESC
;

Ich bin kein Spezialist für den Dialekt von SQL Server. Dies ist eine erste Version für PostrgreSQL (wenn ich das richtig verstehe, kann ich LIMIT 1 / TOP 1 im rekursiven Teil von SQL Server nicht verwenden):

WITH RECURSIVE x AS (
    (SELECT id, val, reset_val, val AS running_total
       FROM reset_runn_total
      ORDER BY id
      LIMIT 1)
    UNION
    (SELECT r.id, r.val, r.reset_val,
            CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END
       FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
      ORDER BY id
      LIMIT 1)
) SELECT * FROM x;

@ JoeObbish um ehrlich zu sein, das ist aus der Frage nicht ganz klar. Die erwarteten Ergebnisse zeigen beispielsweise keine grpSpalte.
Ypercubeᵀᴹ

@ JoeObbish das habe ich auch verstanden. Die Frage könnte jedoch von einer expliziten Aussage dazu profitieren. Der Code in der Frage (mit dem CTE) verwendet ihn auch nicht (und er hat sogar unterschiedlich benannte Spalten). Es wäre für jeden offensichtlich, der die Frage liest - er würde - und sollte - die anderen Antworten oder Kommentare nicht lesen müssen.
Ypercubeᵀᴹ

@ ypercubeᵀᴹ Erforderliche Informationen zur Frage hinzugefügt.
P ரதீப்

1

Es scheint, dass Sie mehrere Fragen / Methoden haben, um das Problem anzugreifen, aber Sie haben uns nicht zur Verfügung gestellt - oder sogar darüber nachgedacht? - die Indizes auf der Tabelle.

Welche Indizes enthält die Tabelle? Ist es ein Heap oder hat es einen Clustered-Index?

Ich würde die verschiedenen vorgeschlagenen Lösungen ausprobieren, nachdem ich diesen Index hinzugefügt habe:

(grp, id) INCLUDE (val, reset_val)

Oder ändern (oder erstellen) Sie einfach den Clustered-Index (grp, id).

Ein Index, der auf die spezifische Abfrage abzielt, sollte die Effizienz verbessern - der meisten, wenn nicht aller Methoden.


Erforderliche Informationen zur Frage hinzugefügt.
P ரதீப்
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.