Oracle
 sql >> Database >  >> RDS >> Oracle

Query SQL per comprimere valori duplicati per intervallo di date

Svilupperò la mia soluzione in modo incrementale, scomponendo ogni trasformazione in una vista. Questo aiuta a spiegare cosa viene fatto e aiuta nel debug e nel test. In sostanza, applica il principio della scomposizione funzionale alle query del database.

Lo farò anche senza utilizzare le estensioni Oracle, con SQL che dovrebbe funzionare su qualsiasi moderno RBDMS. Quindi niente keep, over, partition, solo subquery e raggruppamenti. (Informami nei commenti se non funziona sul tuo RDBMS.)

Innanzitutto, la tabella, che poiché non sono creativo, chiamerò month_value. Poiché l'id non è in realtà un ID univoco, lo chiamerò "eid". Le altre colonne sono "m"onth, "y"ear e "v"alue:

create table month_value( 
   eid int not null, m int, y int,  v int );

Dopo aver inserito i dati, per due eid, ho:

> select * from month_value;
+-----+------+------+------+
| eid | m    | y    | v    |
+-----+------+------+------+
| 100 |    1 | 2008 |   80 |
| 100 |    2 | 2008 |   80 |
| 100 |    3 | 2008 |   90 |
| 100 |    4 | 2008 |   80 |
| 200 |    1 | 2008 |   80 |
| 200 |    2 | 2008 |   80 |
| 200 |    3 | 2008 |   90 |
| 200 |    4 | 2008 |   80 |
+-----+------+------+------+
8 rows in set (0.00 sec)

Successivamente, abbiamo un'entità, il mese, che è rappresentato da due variabili. Dovrebbe essere davvero una colonna (una data o un datetime, o forse anche una chiave esterna per una tabella di date), quindi la trasformeremo in una colonna. Lo faremo come una trasformazione lineare, in modo tale che l'ordinamento sia uguale a (y, m) e tale che per ogni tupla (y, m) ci sia un solo valore e tutti i valori siano consecutivi:

> create view cm_abs_month as 
select *, y * 12 + m as am from month_value;

Questo ci dà:

> select * from cm_abs_month;
+-----+------+------+------+-------+
| eid | m    | y    | v    | am    |
+-----+------+------+------+-------+
| 100 |    1 | 2008 |   80 | 24097 |
| 100 |    2 | 2008 |   80 | 24098 |
| 100 |    3 | 2008 |   90 | 24099 |
| 100 |    4 | 2008 |   80 | 24100 |
| 200 |    1 | 2008 |   80 | 24097 |
| 200 |    2 | 2008 |   80 | 24098 |
| 200 |    3 | 2008 |   90 | 24099 |
| 200 |    4 | 2008 |   80 | 24100 |
+-----+------+------+------+-------+
8 rows in set (0.00 sec)

Ora useremo un self-join in una sottoquery correlata per trovare, per ogni riga, il primo mese successivo in cui il valore cambia. Baseremo questa vista sulla vista precedente che abbiamo creato:

> create view cm_last_am as 
   select a.*, 
    ( select min(b.am) from cm_abs_month b 
      where b.eid = a.eid and b.am > a.am and b.v <> a.v) 
   as last_am 
   from cm_abs_month a;

> select * from cm_last_am;
+-----+------+------+------+-------+---------+
| eid | m    | y    | v    | am    | last_am |
+-----+------+------+------+-------+---------+
| 100 |    1 | 2008 |   80 | 24097 |   24099 |
| 100 |    2 | 2008 |   80 | 24098 |   24099 |
| 100 |    3 | 2008 |   90 | 24099 |   24100 |
| 100 |    4 | 2008 |   80 | 24100 |    NULL |
| 200 |    1 | 2008 |   80 | 24097 |   24099 |
| 200 |    2 | 2008 |   80 | 24098 |   24099 |
| 200 |    3 | 2008 |   90 | 24099 |   24100 |
| 200 |    4 | 2008 |   80 | 24100 |    NULL |
+-----+------+------+------+-------+---------+
8 rows in set (0.01 sec)

last_am è ora il "mese assoluto" del primo (primo) mese (dopo il mese della riga corrente) in cui il valore, v, cambia. È nullo dove non c'è un mese successivo, per quell'eid, nella tabella.

Poiché last_am è lo stesso per tutti i mesi che precedono il cambiamento in v (che si verifica in last_am), possiamo raggruppare su last_am e v (ed eid, ovviamente) e in qualsiasi gruppo, min(am) è l'assoluto mese del primo mese consecutivo che aveva quel valore:

> create view cm_result_data as 
  select eid, min(am) as am , last_am, v 
  from cm_last_am group by eid, last_am, v;

> select * from cm_result_data;
+-----+-------+---------+------+
| eid | am    | last_am | v    |
+-----+-------+---------+------+
| 100 | 24100 |    NULL |   80 |
| 100 | 24097 |   24099 |   80 |
| 100 | 24099 |   24100 |   90 |
| 200 | 24100 |    NULL |   80 |
| 200 | 24097 |   24099 |   80 |
| 200 | 24099 |   24100 |   90 |
+-----+-------+---------+------+
6 rows in set (0.00 sec)

Ora questo è il set di risultati che vogliamo, motivo per cui questa vista è chiamata cm_result_data. Tutto ciò che manca è qualcosa per trasformare i mesi assoluti in (y,m) tuple.

Per farlo, ci uniremo alla tabella month_value.

Ci sono solo due problemi:1) vogliamo il mese prima last_am nel nostro output, e2) abbiamo valori nulli dove non c'è il prossimo mese nei nostri dati; per soddisfare le specifiche del PO, dovrebbero essere intervalli di un mese.

EDIT:Questi potrebbero effettivamente essere intervalli più lunghi di un mese, ma in ogni caso significano che dobbiamo trovare l'ultimo mese per l'eid, che è:

(select max(am) from cm_abs_month d where d.eid = a.eid )

Poiché le viste scompongono il problema, potremmo aggiungere questo "tappo finale" un mese prima, aggiungendo un'altra vista, ma lo inserirò semplicemente nella fusione. Quale sarebbe più efficiente dipende da come il tuo RDBMS ottimizza le query.

Per ottenere un mese prima, ci uniremo (cm_result_data.last_am - 1 =cm_abs_month.am)

Ovunque abbiamo un null, l'OP vuole che il mese "a" sia lo stesso del mese "da", quindi useremo semplicemente coalesce su quello:coalesce( last_am, am). Poiché last elimina tutti i valori null, i nostri join non devono necessariamente essere outer join.

> select a.eid, b.m, b.y, c.m, c.y, a.v 
   from cm_result_data a 
    join cm_abs_month b 
      on ( a.eid = b.eid and a.am = b.am)  
    join cm_abs_month c 
      on ( a.eid = c.eid and 
      coalesce( a.last_am - 1, 
              (select max(am) from cm_abs_month d where d.eid = a.eid )
      ) = c.am)
    order by 1, 3, 2, 5, 4;
+-----+------+------+------+------+------+
| eid | m    | y    | m    | y    | v    |
+-----+------+------+------+------+------+
| 100 |    1 | 2008 |    2 | 2008 |   80 |
| 100 |    3 | 2008 |    3 | 2008 |   90 |
| 100 |    4 | 2008 |    4 | 2008 |   80 |
| 200 |    1 | 2008 |    2 | 2008 |   80 |
| 200 |    3 | 2008 |    3 | 2008 |   90 |
| 200 |    4 | 2008 |    4 | 2008 |   80 |
+-----+------+------+------+------+------+

Unendoci di nuovo otteniamo l'output desiderato dall'OP.

Non che dobbiamo unirci di nuovo. In effetti, la nostra funzione Absolute_month è bidirezionale, quindi possiamo semplicemente ricalcolare l'anno e compensare il mese da esso.

Per prima cosa, prendiamoci cura di aggiungere il mese "end cap":

> create or replace view cm_capped_result as 
select eid, am, 
  coalesce( 
   last_am - 1, 
   (select max(b.am) from cm_abs_month b where b.eid = a.eid)
  ) as last_am, v  
 from cm_result_data a;

E ora otteniamo i dati, formattati per l'OP:

select eid, 
 ( (am - 1) % 12 ) + 1 as sm, 
 floor( ( am - 1 ) / 12 ) as sy, 
 ( (last_am - 1) % 12 ) + 1 as em, 
 floor( ( last_am - 1 ) / 12 ) as ey, v    
from cm_capped_result 
order by 1, 3, 2, 5, 4;

+-----+------+------+------+------+------+
| eid | sm   | sy   | em   | ey   | v    |
+-----+------+------+------+------+------+
| 100 |    1 | 2008 |    2 | 2008 |   80 |
| 100 |    3 | 2008 |    3 | 2008 |   90 |
| 100 |    4 | 2008 |    4 | 2008 |   80 |
| 200 |    1 | 2008 |    2 | 2008 |   80 |
| 200 |    3 | 2008 |    3 | 2008 |   90 |
| 200 |    4 | 2008 |    4 | 2008 |   80 |
+-----+------+------+------+------+------+

E ci sono i dati desiderati dall'OP. Tutto in SQL che dovrebbe essere eseguito su qualsiasi RDBMS ed è scomposto in viste semplici, di facile comprensione e facili da testare.

È meglio rientrare o ricalcolare? Lo lascerò (è una domanda trabocchetto) al lettore.

(Se il tuo RDBMS non consente il raggruppamento nelle visualizzazioni, dovrai prima unirti e poi al gruppo o al gruppo e quindi inserire il mese e l'anno con sottoquery correlate. Questo è lasciato come esercizio per il lettore.)

Jonathan Leffler chiede nei commenti,

Cosa succede con la tua query se ci sono lacune nei dati (diciamo che c'è una voce per il 2007-12 con valore 80 e un'altra per il 2007-10, ma non una per il 2007-11? La domanda non è chiara su cosa dovrebbe succedere lì.

Bene, hai esattamente ragione, l'OP non specifica. Forse c'è una precondizione (non menzionata) che non ci siano lacune. In assenza di un requisito, non dovremmo provare a codificare qualcosa che potrebbe non essere presente. Ma il fatto è che le lacune fanno fallire la strategia del "join back"; la strategia "ricalcola" non fallisce in queste condizioni. Direi di più, ma questo rivelerebbe il trucco nella domanda trabocchetto a cui alludevo sopra.