Patrzyłem na podobne problemy i nigdy nie byłem w stanie znaleźć rozwiązania funkcji okna, które wykonuje jedno przejście przez dane. Nie sądzę, żeby to było możliwe. Funkcje okna muszą być możliwe do zastosowania do wszystkich wartości w kolumnie. To bardzo utrudnia obliczenia resetowania, ponieważ jeden reset zmienia wartość wszystkich poniższych wartości.
Jednym ze sposobów myślenia o problemie jest to, że możesz uzyskać pożądany wynik końcowy, jeśli obliczysz podstawową sumę bieżącą, o ile możesz odjąć sumę bieżącą od poprawnego poprzedniego wiersza. Na przykład w przykładowych danych wartością id
4 jest running total of row 4 - the running total of row 3
. Wartość id
6 wynika z tego, running total of row 6 - the running total of row 3
że reset jeszcze się nie odbył. Wartość id
7 jest taka sama running total of row 7 - the running total of row 6
.
Podchodziłbym do tego z T-SQL w pętli. Trochę mnie poniosło i myślę, że mam pełne rozwiązanie. Dla 3 milionów wierszy i 500 grup kod zakończył się w ciągu 24 sekund na moim pulpicie. Testuję z SQL Server 2016 Developer Edition z 6 vCPU. Korzystam z równoległych wstawek i równoległego wykonywania, więc być może będziesz musiał zmienić kod, jeśli używasz starszej wersji lub masz ograniczenia DOP.
Poniżej kodu, którego użyłem do wygenerowania danych. Zakresy VAL
i RESET_VAL
powinny być podobne do danych przykładowych.
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;
Algorytm wygląda następująco:
1) Zacznij od wstawienia wszystkich wierszy ze standardową sumą bieżącą do tabeli tymczasowej.
2) W pętli:
2a) Dla każdej grupy obliczyć pierwszy wiersz z sumą bieżącą powyżej wartości_resetu pozostającą w tabeli i zapisać identyfikator, sumę roboczą, która była zbyt duża, i poprzednią sumę roboczą, która była zbyt duża w tabeli tymczasowej.
2b) Usuń wiersze z pierwszej tabeli tymczasowej do tabeli wyników tymczasowej, która ma wartość ID
mniejszą lub równą ID
drugiej tabeli tymczasowej. Użyj innych kolumn, aby dostosować bieżącą sumę według potrzeb.
3) Po usunięciu nie przetwarza już wierszy uruchom dodatkową DELETE OUTPUT
tabelę wyników. Dotyczy to wierszy na końcu grupy, które nigdy nie przekraczają wartości resetowania.
Przejdę krok po kroku przez jedną implementację powyższego algorytmu w języku T-SQL.
Zacznij od utworzenia kilku tabel tymczasowych. #initial_results
przechowuje oryginalne dane ze standardową sumą bieżącą, #group_bookkeeping
jest aktualizowany w każdej pętli, aby dowiedzieć się, które wiersze można przenieść, i #final_results
zawiera wyniki z sumą bieżącą skorygowaną dla resetowania.
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;
Po utworzeniu indeksu klastrowego w tabeli tymczasowej wstawianie i budowanie indeksu można wykonywać równolegle. Zrobiłem dużą różnicę na moim komputerze, ale może nie na twoim. Tworzenie indeksu w tabeli źródłowej nie wydawało się pomocne, ale może pomóc na twoim komputerze.
Poniższy kod działa w pętli i aktualizuje tabelę księgowości. Dla każdej grupy musimy znaleźć maksimum, ID
które należy przenieść do tabeli wyników. Potrzebujemy sumę bieżącą z tego wiersza, abyśmy mogli odjąć ją od początkowej sumy bieżącej. grp_done
Kolumna jest ustawiona na 1, gdy nie ma nic więcej do zrobienia dla 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);
Naprawdę nie LOOP JOIN
jestem fanem podpowiedzi w ogóle, ale jest to proste zapytanie i był to najszybszy sposób na uzyskanie tego, czego chciałem. Aby naprawdę zoptymalizować czas odpowiedzi, chciałem złączeń równoległych zagnieżdżonych zamiast połączeń scalających DOP 1.
Poniższy kod działa w pętli i przenosi dane z początkowej tabeli do końcowej tabeli wyników. Zwróć uwagę na korektę początkowej sumy bieżącej.
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;
Dla Twojej wygody poniżej znajduje się pełny kod:
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;