Své řešení budu vyvíjet postupně a každou transformaci rozložím na pohled. To pomáhá vysvětlit, co se dělá, a pomáhá při ladění a testování. Jde v podstatě o aplikaci principu funkčního rozkladu na databázové dotazy.
Udělám to také bez použití rozšíření Oracle, s SQL, které by mělo běžet na jakémkoli moderním RBDMS. Takže žádné keep, over, partition, jen subqueries a group bys. (Pokud to na vašem RDBMS nefunguje, informujte mě v komentářích.)
Nejprve tabulka, kterou, protože nejsem kreativní, budu nazývat měsíční_hodnota. Vzhledem k tomu, že id není ve skutečnosti jedinečné id, budu to nazývat „eid“. Další sloupce jsou "m"měsíc, "y"ear a "v"hodnota:
create table month_value(
eid int not null, m int, y int, v int );
Po vložení dat pro dva eidy mám:
> 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ále máme jednu entitu, měsíc, která je reprezentována jako dvě proměnné. Ve skutečnosti by to měl být jeden sloupec (buď datum nebo datum a čas, nebo možná dokonce cizí klíč k tabulce s daty), takže z toho uděláme jeden sloupec. Uděláme to jako lineární transformaci tak, že třídí stejně jako (y, m) a tak, že pro jakoukoli (y, m) n-tici existuje jedna a jediná hodnota a všechny hodnoty jsou po sobě jdoucí:
> create view cm_abs_month as
select *, y * 12 + m as am from month_value;
To nám dává:
> 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)
Nyní použijeme vlastní spojení v korelovaném poddotazu, abychom pro každý řádek nalezli nejbližší následný měsíc, ve kterém se hodnota změní. Tento pohled založíme na předchozím pohledu, který jsme vytvořili:
> 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 je nyní „absolutní měsíc“ prvního (nejstaršího) měsíce (po měsíci aktuálního řádku), ve kterém se změní hodnota v. Je null tam, kde v tabulce není žádný pozdější měsíc pro daný eid.
Protože last_am je stejný pro všechny měsíce, které vedou ke změně v (která nastane v last_am), můžeme seskupit na last_am a v (a eid, samozřejmě) a v jakékoli skupině je min(am) absolutní měsíc prvního po sobě jdoucího měsíce, který měl tuto hodnotu:
> 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)
Toto je výsledná sada, kterou chceme, a proto se tento pohled nazývá cm_result_data. Jediné, co chybí, je něco, co by transformovalo absolutní měsíce zpět na (y,m) n-tice.
K tomu se připojíme k tabulce měsíční_hodnota.
Existují pouze dva problémy:1) chceme měsíc před last_am v našem výstupu a2) máme null tam, kde v našich datech není žádný další měsíc; pro splnění specifikace OP by to měly být rozsahy jednoho měsíce.
EDIT:Ve skutečnosti to mohou být delší rozsahy než jeden měsíc, ale v každém případě to znamená, že musíme najít poslední měsíc pro eid, což je:
(select max(am) from cm_abs_month d where d.eid = a.eid )
Protože pohledy rozkládají problém, mohli bychom tento „koncový strop“ přidat o měsíc dříve přidáním dalšího pohledu, ale já to vložím do sjednocení. Co by bylo nejúčinnější, závisí na tom, jak váš RDBMS optimalizuje dotazy.
Abychom získali měsíc předem, připojíme se (cm_result_data.last_am - 1 =cm_abs_month.am)
Kdekoli máme hodnotu null, OP chce, aby byl měsíc „do“ stejný jako měsíc „od“, takže na to použijeme sloučení:coalesce( last_am, am). Vzhledem k tomu, že poslední eliminuje jakékoli hodnoty null, naše spojení nemusí být vnější spojení.
> 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 |
+-----+------+------+------+------+------+
Spojením zpět získáme výstup, který OP chce.
Ne, že bychom se museli připojit. Naše funkce absolute_month je obousměrná, takže můžeme jen přepočítat rok a odsadit měsíc od něj.
Nejprve se postarejme o přidání měsíce „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;
A nyní dostáváme data ve formátu podle 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 |
+-----+------+------+------+------+------+
A tam jsou data, která OP chce. Vše v SQL, které by mělo běžet na jakémkoli RDBMS a je rozloženo do jednoduchých, snadno pochopitelných a snadno testovatelných zobrazení.
Je lepší se znovu připojit nebo přepočítat? To (to je záludná otázka) nechám na čtenáři.
(Pokud váš RDBMS nepovoluje seskupování podle zobrazení, budete se muset nejprve připojit a poté seskupit nebo seskupit a poté stáhnout měsíc a rok s korelovanými dílčími dotazy. Toto je ponecháno jako cvičení pro čtenáře.)
ptá se Jonathan Leffler v komentářích,
Co se stane s vaším dotazem, pokud jsou v datech mezery (řekněme, že existuje vstup pro rok 2007-12 s hodnotou 80 a další pro rok 2007-10, ale ne jeden pro rok 2007-11? Otázka není jasná, co by se tam mělo stát.
No, máš naprostou pravdu, OP to nespecifikuje. Možná existuje (nezmíněná) podmínka, že neexistují žádné mezery. Pokud neexistuje požadavek, neměli bychom se snažit kódovat něco, co tam nemusí být. Faktem však je, že kvůli mezerám se strategie „zpětného spojení“ nezdaří; strategie „přepočítat“ za těchto podmínek neselže. Řekl bych více, ale to by odhalilo trik v trikové otázce, na kterou jsem se zmiňoval výše.