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 id
4 beispielsweise der running total of row 4 - the running total of row 3
. Der Wert für id
6 ist der Wert, running total of row 6 - the running total of row 3
da noch kein Reset durchgeführt wurde. Der Wert für id
7 ist der running total of row 7 - the running total of row 6
und 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 VAL
und RESET_VAL
sollten 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 ID
kleiner oder gleich der ID
in 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 OUTPUT
in 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_results
enthält die Originaldaten mit der Standardlaufsumme, #group_bookkeeping
wird in jeder Schleife aktualisiert, um herauszufinden, welche Zeilen verschoben werden können, und #final_results
enthä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, ID
das 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_done
Spalte 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 JOIN
Hinweises 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;