Ho esaminato problemi simili e non sono mai stato in grado di trovare una soluzione con funzioni finestra che esegua un singolo passaggio sui dati. Non penso sia possibile. Le funzioni della finestra devono poter essere applicate a tutti i valori in una colonna. Ciò rende i calcoli di ripristino come questo molto difficili, poiché un ripristino modifica il valore per tutti i seguenti valori.
Un modo di pensare al problema è che puoi ottenere il risultato finale desiderato se calcoli un totale parziale di base purché sia possibile sottrarre il totale parziale dalla riga precedente corretta. Ad esempio, nei dati di esempio il valore per id
4 è il running total of row 4 - the running total of row 3
. Il valore per id
6 è running total of row 6 - the running total of row 3
perché non è stato ancora eseguito un ripristino. Il valore per id
7 è il running total of row 7 - the running total of row 6
e così via.
Mi avvicinerei a questo con T-SQL in un ciclo. Mi sono lasciato trasportare un po 'e penso di avere una soluzione completa. Per 3 milioni di righe e 500 gruppi il codice è terminato in 24 secondi sul mio desktop. Sto testando con SQL Server 2016 Developer Edition con 6 vCPU. Sto sfruttando gli inserti paralleli e l'esecuzione parallela in generale, quindi potrebbe essere necessario modificare il codice se si utilizza una versione precedente o si hanno limitazioni DOP.
Di seguito il codice che ho usato per generare i dati. Gli intervalli sono attivi VAL
e RESET_VAL
dovrebbero essere simili ai dati di esempio.
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;
L'algoritmo è il seguente:
1) Inizia inserendo tutte le righe con un totale parziale standard in una tabella temporanea.
2) In un ciclo:
2a) Per ciascun gruppo, calcolare la prima riga con un totale parziale sopra il valore reset_value rimanente nella tabella e memorizzare l'id, il totale parziale che era troppo grande e il totale parziale precedente che era troppo grande in una tabella temporanea.
2b) Eliminare le righe dalla prima tabella temporanea in una tabella temporanea dei risultati con un valore ID
minore o uguale a quello ID
della seconda tabella temporanea. Utilizzare le altre colonne per regolare il totale parziale secondo necessità.
3) Dopo che l'eliminazione non elabora più le righe, eseguine un'ulteriore DELETE OUTPUT
nella tabella dei risultati. Questo è per le righe alla fine del gruppo che non superano mai il valore di reset.
Analizzerò passo per passo un'implementazione dell'algoritmo sopra in T-SQL.
Inizia creando alcune tabelle temporanee. #initial_results
contiene i dati originali con il totale parziale standard, #group_bookkeeping
viene aggiornato ogni ciclo per capire quali righe possono essere spostate e #final_results
contiene i risultati con il totale parziale rettificato per i ripristini.
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;
Dopo di che creo l'indice cluster sulla tabella temporanea, quindi l'inserimento e la creazione dell'indice possono essere eseguiti in parallelo. Ha fatto una grande differenza sulla mia macchina ma potrebbe non sulla tua. La creazione di un indice nella tabella di origine non sembra essere d'aiuto, ma ciò potrebbe essere d'aiuto sul tuo computer.
Il codice seguente viene eseguito nel ciclo e aggiorna la tabella di contabilità. Per ogni gruppo, dobbiamo ottenere il massimo ID
da trovare nella tabella dei risultati. Abbiamo bisogno del totale parziale da quella riga in modo da poterlo sottrarre dal totale parziale iniziale. La grp_done
colonna è impostata su 1 quando non c'è più lavoro da fare per a 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);
In realtà non è un fan del LOOP JOIN
suggerimento in generale, ma questa è una domanda semplice ed è stato il modo più veloce per ottenere quello che volevo. Per ottimizzare davvero i tempi di risposta, volevo join loop nidificati paralleli anziché join merge DOP 1.
Il codice seguente viene eseguito nel ciclo e sposta i dati dalla tabella iniziale alla tabella dei risultati finali. Si noti l'adeguamento al totale parziale iniziale.
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;
Per comodità, di seguito è riportato il codice completo:
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;