Ik ga mijn oplossing stapsgewijs ontwikkelen, waarbij ik elke transformatie ontbind in een weergave. Dit helpt zowel bij het uitleggen wat er wordt gedaan als bij het debuggen en testen. Het past in wezen het principe van functionele decompositie toe op databasequery's.
Ik ga het ook doen zonder Oracle-extensies te gebruiken, met SQL die op elk modern RBDMS zou moeten draaien. Dus geen keep, over, partition, alleen subquery's en group bys. (Laat het me weten in de reacties als het niet werkt op je RDBMS.)
Ten eerste de tabel, die ik, aangezien ik niet creatief ben, Month_value zal noemen. Aangezien de id niet echt een unieke id is, noem ik het "eid". De andere kolommen zijn "m"onth, "y"ear en "v"alue:
create table month_value(
eid int not null, m int, y int, v int );
Na het invoeren van de gegevens, voor twee eid's, heb ik:
> 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)
Vervolgens hebben we één entiteit, de maand, die wordt weergegeven als twee variabelen. Dat zou eigenlijk één kolom moeten zijn (een datum of een datetime, of misschien zelfs een vreemde sleutel naar een tabel met datums), dus we maken er één kolom van. We doen dat als een lineaire transformatie, zodat het hetzelfde sorteert als (y, m), en zodanig dat voor elke (y,m) tuple er één en enige waarde is, en alle waarden zijn opeenvolgend:
> create view cm_abs_month as
select *, y * 12 + m as am from month_value;
Dat geeft ons:
> 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 gebruiken we een self-join in een gecorreleerde subquery om voor elke rij de vroegste volgende maand te vinden waarin de waarde verandert. We baseren deze weergave op de vorige weergave die we hebben gemaakt:
> 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 is nu de "absolute maand" van de eerste (vroegste) maand (na de maand van de huidige rij) waarin de waarde, v, verandert. Het is nul als er geen latere maand is, voor die eid, in de tabel.
Aangezien last_am hetzelfde is voor alle maanden voorafgaand aan de verandering in v (die plaatsvindt om last_am), kunnen we groeperen op last_am en v (en eid natuurlijk), en in elke groep is de min(am) de absolute maand van de eerste opeenvolgende maand die die waarde had:
> 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)
Dit is de resultaatset die we willen, daarom wordt deze weergave cm_result_data genoemd. Het enige dat ontbreekt, is iets om absolute maanden terug te transformeren naar (y,m) tuples.
Om dat te doen, voegen we ons bij de tabel Month_value.
Er zijn slechts twee problemen:1) we willen de maand voor last_am in onze uitvoer, en 2) we hebben nulls waar er geen volgende maand in onze gegevens staat; om aan de specificatie van de OP te voldoen, moeten dit bereiken van één maand zijn.
EDIT:Dit kunnen eigenlijk langere bereiken zijn dan een maand, maar in elk geval betekenen ze dat we de laatste maand voor de eid moeten vinden, namelijk:
(select max(am) from cm_abs_month d where d.eid = a.eid )
Omdat de weergaven het probleem oplossen, zouden we deze "eindlimiet" maand eerder kunnen toevoegen door een andere weergave toe te voegen, maar ik zal dit gewoon invoegen in de samenvoeging. Wat het meest efficiënt is, hangt af van hoe uw RDBMS zoekopdrachten optimaliseert.
Om een maand eerder te krijgen, doen we mee (cm_result_data.last_am - 1 =cm_abs_month.am)
Overal waar we een null hebben, wil het OP dat de "tot"-maand hetzelfde is als de "van"-maand, dus we gebruiken daar gewoon coalesce op:coalesce(last_am, am). Omdat laatste alle nulls elimineert, hoeven onze joins geen outer joins te zijn.
> 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 |
+-----+------+------+------+------+------+
Door terug te komen, krijgen we de output die de OP wil.
Niet dat we terug moeten. Toevallig is onze functie absoluut_maand bidirectioneel, dus we kunnen het jaar gewoon opnieuw berekenen en de maand ervan verrekenen.
Laten we eerst zorgen voor het toevoegen van de "eindlimiet"-maand:
> 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;
En nu krijgen we de gegevens, geformatteerd volgens de 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 |
+-----+------+------+------+------+------+
En er zijn de gegevens die de OP wil. Alles in SQL dat op elk RDBMS zou moeten draaien en is opgesplitst in eenvoudige, gemakkelijk te begrijpen en gemakkelijk te testen weergaven.
Is het beter om opnieuw mee te doen of om te herberekenen? Ik laat dat (het is een strikvraag) aan de lezer over.
(Als uw RDBMS geen group-bys in weergaven toestaat, moet u eerst lid worden en dan groeperen, of groeperen en vervolgens de maand en het jaar invoeren met gecorreleerde subquery's. Dit wordt overgelaten als een oefening voor de lezer.)
Jonathan Leffler vraagt in de reacties,
Wat gebeurt er met uw zoekopdracht als er hiaten in de gegevens zijn (stel dat er een invoer is voor 2007-12 met waarde 80 en een andere voor 2007-10, maar niet één voor 2007-11? De vraag is niet duidelijk wat daar moet gebeuren.
Nou, je hebt helemaal gelijk, de OP specificeert niet. Misschien is er een (niet genoemde) voorwaarde dat er geen hiaten zijn. Als er geen vereiste is, moeten we niet proberen iets te coderen dat er misschien niet is. Maar het feit is dat lacunes ervoor zorgen dat de "joining back"-strategie faalt; de "herbereken"-strategie faalt niet onder die omstandigheden. Ik zou meer zeggen, maar dat zou de truc onthullen in de strikvraag waar ik hierboven op gezinspeeld heb.