Aggrega solo i record adiacenti con T-SQL
-
04-07-2019 - |
Domanda
Ho (semplificato per l'esempio) una tabella con i seguenti dati
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-04 01 38
4 2008-10-04 2008-10-05 01 23
5 2008-10-05 2008-10-06 03 14
6 2008-10-06 2008-10-07 02 3
7 2008-10-07 2008-10-08 02 8
8 2008-10-08 2008-11-08 03 19
Le date rappresentano un periodo di tempo, l'ID è lo stato in cui si trovava un sistema durante quel periodo e l'importo è un valore correlato a quello stato.
Quello che voglio fare è aggregare gli importi per file adiacenti con lo stesso numero ID stesso , ma mantenere la stessa sequenza generale in modo da poter combinare percorsi contigui. Quindi voglio finire con dati come:
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-05 01 61
4 2008-10-05 2008-10-06 03 14
5 2008-10-06 2008-10-08 02 11
6 2008-10-08 2008-11-08 03 19
Sto cercando una soluzione T-SQL che può essere inserita in un SP, tuttavia non riesco a vedere come farlo con semplici query. Sospetto che possa richiedere iterazioni di qualche tipo, ma non voglio seguire questa strada.
Il motivo per cui voglio fare questa aggregazione è che il prossimo passo nel processo è fare un SUM () e Count () raggruppati per gli ID univoci che si verificano all'interno della sequenza, in modo che i miei dati finali siano simili a :
ID Counts Total
-- ------ -----
01 2 71
02 2 31
03 2 33
Tuttavia, se faccio un semplice
SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID
Nella tabella originale ottengo qualcosa di simile
ID Counts Total
-- ------ -----
01 3 71
02 3 31
03 2 33
Che non è quello che voglio.
Nessuna soluzione corretta
Altri suggerimenti
Se leggi il libro " Sviluppo di applicazioni di database orientate al tempo in SQL " di RT Snodgrass (il cui pdf è disponibile sul suo sito web sotto pubblicazioni), e arrivando alla Figura 6.25 a p165-166, troverete l'SQL non banale che può essere usato nell'esempio corrente per raggruppare le varie righe con lo stesso valore ID e intervalli di tempo continui.
Lo sviluppo della query di seguito è quasi corretto, ma alla fine c'è un problema individuato che ha la sua origine nella prima istruzione SELECT. Non ho ancora rintracciato il motivo per cui viene data la risposta errata. [Se qualcuno può testare l'SQL sul proprio DBMS e dirmi se la prima query funziona correttamente lì, sarebbe di grande aiuto!]
Sembra qualcosa del tipo:
-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
Start DATE,
Finish DATE,
ID CHAR(2),
Amount INT
);
INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02', 3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02', 8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);
SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)));
L'output di quella query è:
01 2008-10-01 2008-10-02
01 2008-10-03 2008-10-05
02 2008-10-02 2008-10-03
02 2008-10-06 2008-10-08
03 2008-10-05 2008-10-06
03 2008-10-05 2008-11-08
03 2008-10-08 2008-11-08
Modificato : c'è un problema con la penultima riga: non dovrebbe esserci. E non sono chiaro (ancora) da dove provenga.
Ora dobbiamo considerare quell'espressione complessa come un'espressione di query nella clausola FROM di un'altra istruzione SELECT, che sommerà i valori di quantità per un determinato ID sulle voci che si sovrappongono agli intervalli massimi mostrati sopra.
SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)))) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
ORDER BY M.ID, M.Start;
Questo dà:
ID Start Finish Amount
01 2008-10-01 2008-10-02 10
01 2008-10-03 2008-10-05 61
02 2008-10-02 2008-10-03 20
02 2008-10-06 2008-10-08 11
03 2008-10-05 2008-10-06 14
03 2008-10-05 2008-11-08 33 -- Here be trouble!
03 2008-10-08 2008-11-08 19
Modificato : questo è quasi il set di dati corretto su cui eseguire l'aggregazione COUNT e SUM richiesta dalla domanda originale, quindi la risposta finale è:
SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS
(SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish) OR
(T2.Start <= L.Finish AND L.Finish < T2.Finish)))
) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
) AS I
GROUP BY I.ID
ORDER BY I.ID;
id number amount
01 2 71
02 2 31
03 3 66
Recensione : Oh! Drat ... la voce per 3 ha il doppio dell '"importo" che dovrebbe avere. Le parti "modificate" precedenti indicano dove le cose hanno iniziato a non funzionare. Sembra che la prima query sia sottilmente sbagliata (forse è destinata a una domanda diversa) o che l'ottimizzatore con cui sto lavorando si sta comportando male. Tuttavia, dovrebbe esserci una risposta strettamente correlata a ciò che fornirà i valori corretti.
Per la cronaca: testato su IBM Informix Dynamic Server 11.50 su Solaris 10. Tuttavia, dovrebbe funzionare perfettamente su qualsiasi altro DBMS SQL moderatamente conforme agli standard.
Probabilmente devi creare un cursore e scorrere i risultati, tenendo traccia dell'id con cui stai lavorando e accumulando i dati lungo il percorso. Quando l'id cambia, è possibile inserire i dati accumulati in una tabella temporanea e restituire la tabella al termine della procedura (selezionare tutto da essa). Una funzione basata su tabella potrebbe essere migliore in quanto puoi semplicemente inserirla nella tabella di ritorno mentre procedi.
Sospetto che possa richiedere iterazioni di qualche tipo, ma non voglio seguire questa strada.
Penso che sia la strada da percorrere, usa un cursore per popolare una variabile di tabella. Se si dispone di un numero elevato di record, è possibile utilizzare una tabella permanente per archiviare i risultati, quindi quando è necessario recuperare i dati è possibile elaborare solo i nuovi dati.
Vorrei aggiungere un campo bit con un valore predefinito di 0 alla tabella di origine per tenere traccia di quali record sono stati elaborati. Supponendo che nessuno stia utilizzando select * nella tabella, l'aggiunta di una colonna con un valore predefinito non influirà sul resto dell'applicazione.
Aggiungi un commento a questo post se desideri aiuto per codificare la soluzione.
Bene, ho deciso di seguire la rotta dell'iterazione usando una combinazione di join e cursori. Unendo la tabella dei dati con se stesso posso creare un elenco di collegamenti solo di quei record che sono consecutivi.
INSERT INTO #CONSEC
SELECT a.ID, a.Start, b.Finish, b.Amount
FROM Data a JOIN Data b
ON (a.Finish = b.Start) AND (a.ID = b.ID)
Quindi posso sbloccare l'elenco ripetendo su di esso con un cursore e facendo gli aggiornamenti alla tabella dei dati per regolare (ed eliminare i record ora estranei dalla tabella dei dati)
DECLARE CCursor CURSOR FOR
SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC
@Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
WHILE @FETCH_STATUS = 0
BEGIN
@Total = @Total + @Amount
@Start_Last = @Start
@Finish_Last = @Finish
@ID_Last = @ID
DELETE FROM Data WHERE Start = @Finish
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
IF (@ID_Last<> @ID) OR (@Finish<>@Start_Last)
BEGIN
UPDATE Data
SET Amount = Amount + @Total
WHERE Start = @Start_Last
@Total = 0
END
END
CLOSE CCursor
DEALLOCATE CCursor
Tutto funziona e ha prestazioni accettabili per i dati tipici che sto usando.
Ho riscontrato un piccolo problema con il codice sopra. Inizialmente stavo aggiornando la tabella dei dati su ciascun loop attraverso il cursore. Ma questo non ha funzionato. Sembra che sia possibile eseguire un solo aggiornamento su un record e che più aggiornamenti (per continuare ad aggiungere dati) tornino alla lettura del contenuto originale del record.