sql >> Databasteknik >  >> RDS >> Oracle

SQL-fråga för att komprimera dubbletter av värden efter datumintervall

Jag kommer att utveckla min lösning stegvis och bryta ner varje transformation till en vy. Detta både hjälper till att förklara vad som görs och hjälper till vid felsökning och testning. Det är i huvudsak att tillämpa principen om funktionell nedbrytning på databasfrågor.

Jag kommer också att göra det utan att använda Oracle-tillägg, med SQL som borde köras på alla moderna RBDMS. Så ingen keep, over, partition, bara underfrågor och gruppbys. (Meddela mig i kommentarerna om det inte fungerar på din RDBMS.)

Först tabellen, som eftersom jag är okreativ, kommer jag att kalla månad_värde. Eftersom id:t egentligen inte är ett unikt id, kallar jag det "eid". De andra kolumnerna är "m"onth, "y"ear och "v"alue:

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

Efter att ha infogat data, för två eids, har jag:

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

Därefter har vi en enhet, månaden, som representeras som två variabler. Det borde egentligen vara en kolumn (antingen ett datum eller en datetime, eller kanske till och med en främmande nyckel till en tabell över datum), så vi gör det till en kolumn. Vi kommer att göra det som en linjär transformation, så att den sorterar samma som (y, m), och så att det för alla (y,m) tupel finns ett och enda värde, och alla värden är konsekutiva:

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

Det ger oss:

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

Nu kommer vi att använda en självanslutning i en korrelerad underfråga för att för varje rad hitta den tidigaste efterföljande månaden då värdet ändras. Vi kommer att basera den här vyn på den tidigare vyn vi skapade:

> 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 är nu den "absoluta månaden" i den första (tidigaste) månaden (efter månaden för den aktuella raden) där värdet v ändras. Den är null där det inte finns någon senare månad, för den eid, i tabellen.

Eftersom last_am är densamma för alla månader som leder fram till förändringen i v (som inträffar vid last_am), kan vi gruppera på last_am och v (och eid, naturligtvis), och i vilken grupp som helst är min(am) den absoluta månaden den första månad i följd som hade det värdet:

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

Nu är detta den resultatuppsättning vi vill ha, varför denna vy kallas cm_result_data. Allt som saknas är något att förvandla absoluta månader tillbaka till (y,m) tupler.

För att göra det går vi bara med i tabellen month_value.

Det finns bara två problem:1) vi vill ha månaden före last_am i vår utdata, och2) vi har nollvärden där det inte finns någon nästa månad i vår data; för att uppfylla OP:s specifikation bör dessa vara en månadsintervall.

EDIT:Dessa kan faktiskt vara längre intervall än en månad, men i alla fall innebär de att vi måste hitta den senaste månaden för eid, vilket är:

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

Eftersom vyerna bryter ner problemet, kunde vi lägga till denna "slutkapsel" månad tidigare, genom att lägga till en annan vy, men jag ska bara infoga detta i sammansmältningen. Vilket som skulle vara mest effektivt beror på hur din RDBMS optimerar frågor.

För att få månaden innan går vi med (cm_result_data.last_am - 1 =cm_abs_month.am)

Varhelst vi har en noll, vill OP att "till"-månaden ska vara densamma som "från"-månaden, så vi använder bara sammansmältning på det:coalesce( last_am, am). Eftersom senast eliminerar eventuella nollor, behöver våra joins inte vara outer joins.

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

Genom att gå tillbaka får vi den produktion som OP vill ha.

Inte för att vi måste gå tillbaka. Som det händer är vår absolute_month-funktion dubbelriktad, så vi kan bara räkna om året och kompensera månaden från det.

Först, låt oss ta hand om att lägga till "end cap"-månaden:

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

Och nu får vi data, formaterade enligt 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 |
+-----+------+------+------+------+------+

Och där är de uppgifter som OP vill ha. Allt i SQL som bör köras på alla RDBMS och delas upp i enkla, lättförståeliga och lätta att testa vyer.

Är det bättre att gå med igen eller att räkna om? Jag lämnar det (det är en trickfråga) till läsaren.

(Om din RDBMS inte tillåter gruppbys i vyer, måste du gå med först och sedan gruppera, eller grupp och sedan dra in månaden och året med korrelerade underfrågor. Detta lämnas som en övning för läsaren.)

frågar Jonathan Leffler i kommentarerna,

Vad händer med din fråga om det finns luckor i data (säg att det finns en post för 2007-12 med värde 80, och en annan för 2007-10, men inte en för 2007-11? Frågan är inte tydlig vad som ska hända där.

Tja, du har helt rätt, OP specificerar inte. Kanske finns det en (onämnd) förutsättning att det inte finns några luckor. I avsaknad av krav bör vi inte försöka koda runt något som kanske inte finns där. Men faktum är att luckor gör att strategin "att gå tillbaka" misslyckas; "beräkna om"-strategin misslyckas inte under dessa förhållanden. Jag skulle säga mer, men det skulle avslöja tricket i trickfrågan jag anspelade på ovan.



  1. MySQL beviljar privilegier till användare för databas

  2. Kopiera tabell till en annan databas på en annan SQL Server

  3. Installera MariaDB på en Mac

  4. Fyller Many2many-fältet (odoo 8)