Ich werde meine Lösung schrittweise entwickeln und jede Transformation in eine Ansicht zerlegen. Dies hilft sowohl zu erklären, was getan wird, als auch beim Debuggen und Testen. Es wendet im Wesentlichen das Prinzip der funktionalen Zerlegung auf Datenbankabfragen an.
Ich werde es auch ohne die Verwendung von Oracle-Erweiterungen tun, mit SQL, das auf jedem modernen RBDMS laufen sollte. Also kein Keep, Over, Partition, nur Subqueries und Group Bys. (Informiere mich in den Kommentaren, wenn es auf deinem RDBMS nicht funktioniert.)
Zuerst die Tabelle, die ich, da ich unkreativ bin, month_value nenne. Da die ID eigentlich keine eindeutige ID ist, nenne ich sie "eid". Die anderen Spalten sind "Monat", "Jahr" und "Wert":
create table month_value(
eid int not null, m int, y int, v int );
Nach dem Einfügen der Daten habe ich für zwei EIDs:
> 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)
Als nächstes haben wir eine Entität, den Monat, die als zwei Variablen dargestellt wird. Das sollte wirklich eine Spalte sein (entweder ein Datum oder eine Datumszeit oder vielleicht sogar ein Fremdschlüssel zu einer Datumstabelle), also machen wir daraus eine Spalte. Wir machen das als lineare Transformation, so dass es genauso sortiert wird wie (y, m) und dass es für jedes (y, m)-Tupel nur einen einzigen Wert gibt und alle Werte aufeinander folgen:
> create view cm_abs_month as
select *, y * 12 + m as am from month_value;
Das gibt uns:
> 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)
Jetzt verwenden wir einen Self-Join in einer korrelierten Unterabfrage, um für jede Zeile den frühesten Folgemonat zu finden, in dem sich der Wert ändert. Wir basieren diese Ansicht auf der vorherigen Ansicht, die wir erstellt haben:
> 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 ist jetzt der "absolute Monat" des ersten (frühesten) Monats (nach dem Monat der aktuellen Zeile), in dem sich der Wert v ändert. Es ist null, wenn es in der Tabelle keinen späteren Monat für diese Eid gibt.
Da last_am für alle Monate vor der Änderung von v (die bei last_am auftritt) gleich ist, können wir nach last_am und v (und natürlich eid) gruppieren, und in jeder Gruppe ist min(am) das Absolute Monat des ersten aufeinanderfolgender Monat mit diesem Wert:
> 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)
Dies ist nun die gewünschte Ergebnismenge, weshalb diese Ansicht cm_result_data heißt. Alles, was fehlt, ist etwas, um absolute Monate wieder in (y,m)-Tupel umzuwandeln.
Dazu verbinden wir uns einfach mit der Tabelle month_value.
Es gibt nur zwei Probleme:1) Wir wollen den Monat vorher last_am in unserer Ausgabe, und2) wir haben Nullen, wo es keinen nächsten Monat in unseren Daten gibt; Um die Spezifikation des OP zu erfüllen, sollten dies einzelne Monatsbereiche sein.
BEARBEITEN:Dies könnten tatsächlich längere Bereiche als ein Monat sein, aber in jedem Fall bedeuten sie, dass wir den letzten Monat für die EID finden müssen, der lautet:
(select max(am) from cm_abs_month d where d.eid = a.eid )
Da die Ansichten das Problem zerlegen, könnten wir diese "Endkappe" einen Monat früher hinzufügen, indem wir eine weitere Ansicht hinzufügen, aber ich füge diese einfach in die Koaleszenz ein. Was am effizientesten wäre, hängt davon ab, wie Ihr RDBMS Abfragen optimiert.
Um einen Monat vorher zu bekommen, verbinden wir uns mit (cm_result_data.last_am - 1 =cm_abs_month.am)
Überall dort, wo wir eine Null haben, möchte das OP, dass der „bis“-Monat derselbe ist wie der „von“-Monat, also verwenden wir einfach coalesce dafür:coalesce( last_am, am). Da last alle Nullen eliminiert, müssen unsere Verknüpfungen keine äußeren Verknüpfungen sein.
> 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 |
+-----+------+------+------+------+------+
Indem wir uns wieder anschließen, erhalten wir die Ausgabe, die das OP will.
Nicht, dass wir uns wieder anschließen müssen. Zufälligerweise ist unsere absolute_month-Funktion bidirektional, sodass wir einfach das Jahr neu berechnen und den Monat daraus versetzen können.
Lassen Sie uns zunächst den "Endkappen"-Monat hinzufügen:
> 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;
Und jetzt bekommen wir die Daten, formatiert nach 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 |
+-----+------+------+------+------+------+
Und da sind die Daten, die das OP will. Alles in SQL, das auf jedem RDBMS laufen sollte und in einfache, leicht verständliche und leicht zu testende Ansichten zerlegt ist.
Ist es besser, wieder beizutreten oder neu zu berechnen? Das überlasse ich (es ist eine Fangfrage) dem Leser.
(Wenn Ihr RDBMS keine Group Bys in Ansichten zulässt, müssen Sie zuerst joinen und dann group oder group und dann den Monat und das Jahr mit korrelierten Unterabfragen abrufen. Dies bleibt dem Leser als Übung überlassen.)
Jonathan Leffler fragt in den Kommentaren,
Was passiert mit Ihrer Abfrage, wenn es Lücken in den Daten gibt (sagen wir, es gibt einen Eintrag für 2007-12 mit dem Wert 80 und einen anderen für 2007-10, aber keinen für 2007-11? Die Frage ist nicht klar, was dort passieren soll.
Nun, Sie haben genau Recht, das OP gibt es nicht an. Vielleicht gibt es eine (unerwähnte) Voraussetzung, dass es keine Lücken gibt. In Ermangelung einer Anforderung sollten wir nicht versuchen, etwas zu codieren, das möglicherweise nicht vorhanden ist. Tatsache ist jedoch, dass Lücken die „Joining Back“-Strategie scheitern lassen; die Strategie "Neu berechnen" schlägt unter diesen Bedingungen nicht fehl. Ich würde mehr sagen, aber das würde den Trick in der Fangfrage enthüllen, auf die ich oben angespielt habe.