Ripristina totale parziale in base a un'altra colonna


10

Sto cercando di calcolare il totale parziale. Ma dovrebbe reimpostare quando la somma cumulativa è maggiore di un altro valore di colonna

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

Dettagli indice:

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

dati di esempio

+----+-----+-----------+-----+
| 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   |
+----+-----+-----------+-----+ 

Risultato atteso

+----+-----+-----------------+-------------+
| 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
+----+-----+-----------------+-------------+

Query:

Ho ottenuto il risultato usando Recursive CTE. La domanda originale è qui /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 

Esiste un'alternativa migliore T-SQLsenza usare CLR.


Meglio come? Questa query mostra scarse prestazioni? Usando quali metriche?
Aaron Bertrand

@AaronBertrand - Per una migliore comprensione ho pubblicato dati di esempio per un solo gruppo. Devo fare lo stesso per i 50000gruppi con 60 ID . quindi il conteggio totale dei record sarà in giro 3000000. Sono sicuro Recursive CTEche non si ridimensionerà bene 3000000. Aggiornerò le metriche quando torno in ufficio. Possiamo ottenerlo usando sum()Over(Order by)come hai usato in questo articolo sqlperformance.com/2012/07/t-sql-queries/running-totals
P ரதீப்

Un cursore potrebbe fare meglio di un CTE ricorsivo
paparazzo,

Risposte:


6

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 id4 è il running total of row 4 - the running total of row 3. Il valore per id6 è running total of row 6 - the running total of row 3perché non è stato ancora eseguito un ripristino. Il valore per id7 è il running total of row 7 - the running total of row 6e 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 VALe RESET_VALdovrebbero 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 IDminore o uguale a quello IDdella 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 OUTPUTnella 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_resultscontiene i dati originali con il totale parziale standard, #group_bookkeepingviene aggiornato ogni ciclo per capire quali righe possono essere spostate e #final_resultscontiene 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 IDda trovare nella tabella dei risultati. Abbiamo bisogno del totale parziale da quella riga in modo da poterlo sottrarre dal totale parziale iniziale. La grp_donecolonna è 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 JOINsuggerimento 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;

semplicemente fantastico, ti ricompenserò con generosità
P ரதீப்

Nel nostro server, per 50000 grp e 60 id il tuo ha impiegato 1 minuto e 10 secondi. Recursive CTEci sono voluti 2 minuti e 15 secondi
P ரதீப்

Ho testato entrambi i codici con gli stessi dati. Il tuo è stato fantastico. Può essere ulteriormente migliorato?
P ரதீப்

Volevo dire, ho eseguito il tuo codice sui nostri dati reali e l'ho testato. Il calcolo viene elaborato nelle tabelle temporanee nella mia procedura reale, molto probabilmente dovrebbe essere impacchettato. Sarà buono se può essere ridotto a circa 30 secondi circa
P ரதீப்

@Prdp Ho provato un approccio rapido che utilizzava un aggiornamento ma sembrava essere peggio. Non potremo approfondire questo per un po '. Prova a registrare quanto tempo impiega ciascuna operazione per capire quale parte sta funzionando più lentamente sul tuo server. È sicuramente possibile che ci sia un modo per accelerare questo codice o un algoritmo migliore in generale.
Joe Obbish,

4

Utilizzando un CURSORE:

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      |
+----+-----+-----------+-------------+

Controlla qui: http://rextester.com/WSPLO95303


3

Versione non finita ma pura di SQL:

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
;

Non sono uno specialista del dialetto di SQL Server. Questa è una versione iniziale di PostrgreSQL (se ho capito bene non posso usare LIMIT 1 / TOP 1 nella parte ricorsiva in SQL Server):

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 a dire il vero, non è del tutto chiaro dalla domanda. I risultati previsti, ad esempio, non mostrano grpcolonne.
ypercubeᵀᴹ

@JoeObbish è anche quello che ho capito. tuttavia, la domanda potrebbe beneficiare di una dichiarazione esplicita al riguardo. Il codice nella domanda (con il CTE) non lo usa neanche (e ha anche colonne con nomi diversi). Sarebbe ovvio per chiunque leggesse la domanda - non avrebbero - e non dovrebbero - leggere le altre risposte o commenti.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Aggiunte le informazioni richieste sulla domanda.
P ரதீப்

1

Sembra che tu abbia diverse domande / metodi per attaccare il problema ma non ci hai fornito - o addirittura preso in considerazione? - gli indici sul tavolo.

Quali indici ci sono nella tabella? È un heap o ha un indice cluster?

Vorrei provare le varie soluzioni suggerite dopo aver aggiunto questo indice:

(grp, id) INCLUDE (val, reset_val)

O semplicemente modificare (o creare) l'indice cluster per essere (grp, id).

Avere un indice destinato alla query specifica dovrebbe migliorare l'efficienza, della maggior parte se non di tutti i metodi.


Aggiunte le informazioni richieste sulla domanda.
P ரதீப்
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.