Ще развивам решението си постепенно, като разлагам всяка трансформация в изглед. Това едновременно помага да се обясни какво се прави и помага при отстраняване на грешки и тестване. По същество се прилага принципът на функционална декомпозиция към заявките към база данни.
Също така ще го направя без да използвам разширения на Oracle, със SQL, който трябва да работи на всеки съвременен RBDMS. Така че няма задържане, над, разделяне, само подзаявки и групови bys. (Информирайте ме в коментарите, ако не работи на вашата RDBMS.)
Първо, таблицата, която тъй като не съм креативен, ще нарека month_value. Тъй като идентификаторът всъщност не е уникален идентификатор, ще го нарека "eid". Другите колони са "m"onth, "y"ear и "v"alue:
create table month_value(
eid int not null, m int, y int, v int );
След въвеждане на данните, за две 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)
След това имаме един обект, месецът, който е представен като две променливи. Това наистина трябва да е една колона (или дата, или дата и час, или може би дори външен ключ към таблица с дати), така че ще я направим една колона. Ще направим това като линейна трансформация, така че да сортира същото като (y, m) и така, че за всеки (y,m) кортеж има една и единствена стойност и всички стойности са последователни:
> create view cm_abs_month as
select *, y * 12 + m as am from month_value;
Това ни дава:
> 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)
Сега ще използваме самостоятелно присъединяване в корелирана подзаявка, за да намерим за всеки ред най-ранния следващ месец, в който стойността се променя. Ще базираме този изглед на предишния изглед, който създадохме:
> 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 сега е "абсолютният месец" на първия (най-ранния) месец (след месеца на текущия ред), в който стойността v се променя. То е нула, когато в таблицата няма по-късен месец за този eid.
Тъй като last_am е едно и също за всички месеци, водещи до промяната в v (която се случва в last_am), можем да групираме по last_am и v (и eid, разбира се), и във всяка група min(am) е абсолютното месец на първия пореден месец, който имаше тази стойност:
> 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)
Сега това е наборът от резултати, който искаме, поради което този изглед се нарича cm_result_data. Всичко, което липсва, е нещо, което да трансформира абсолютните месеци обратно в (y,m) кортежи.
За да направим това, просто ще се присъединим към таблицата month_value.
Има само два проблема:1) искаме месец преди last_am в нашия изход и2) имаме нулеви стойности, където в нашите данни няма следващия месец; за да отговарят на спецификацията на ОП, те трябва да са едномесечни диапазони.
РЕДАКТИРАНЕ:Това всъщност може да са по-дълги обхвати от един месец, но във всеки случай те означават, че трябва да намерим последния месец за eid, който е:
(select max(am) from cm_abs_month d where d.eid = a.eid )
Тъй като изгледите разграждат проблема, бихме могли да добавим тази „крайна шапка“ месец по-рано, като добавим друг изглед, но просто ще вмъкна това в обединението. Кое би било най-ефективно зависи от това как вашата RDBMS оптимизира заявките.
За да получите месец преди това, ще се присъединим (cm_result_data.last_am - 1 =cm_abs_month.am)
Където и да имаме нула, ОП иска месецът "до" да бъде същият като месеца "от", така че просто ще използваме coalesce за това:coalesce( last_am, am). Тъй като last елиминира всички нули, не е необходимо нашите съединения да бъдат външни съединения.
> 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 |
+-----+------+------+------+------+------+
Като се присъединим обратно, получаваме изхода, който OP иска.
Не че трябва да се присъединим обратно. Както се случва, нашата функция absolute_month е двупосочна, така че можем просто да преизчислим годината и да изместим месеца от нея.
Първо, нека се погрижим за добавянето на месец „крайна шапка“:
> 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;
И сега получаваме данните, форматирани според 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 |
+-----+------+------+------+------+------+
И ето данните, които ОП иска. Всичко в SQL, което трябва да работи на всяка RDBMS и е разложено на прости, лесни за разбиране и лесни за тестване изгледи.
По-добре ли е да се присъедините отново или да преизчислите? Ще оставя това (това е трик въпрос) на читателя.
(Ако вашата RDBMS не позволява групиране в изгледи, ще трябва първо да се присъедините и след това да групирате или групирате и след това да изтеглите месеца и годината с корелирани подзаявки. Това е оставено като упражнение за читателя.)
Джонатан Лефлър пита в коментарите,
Какво се случва с вашата заявка, ако има пропуски в данните (да речем, че има запис за 2007-12 със стойност 80 и друг за 2007-10, но не и за 2007-11? Въпросът не е ясен какво трябва да се случи там.
Е, абсолютно си прав, ОП не уточнява. Може би има (неспоменато) предварително условие да няма пропуски. При липса на изискване не трябва да се опитваме да кодираме нещо, което може да не е там. Но факт е, че пропуските правят стратегията за "връщане" неуспешна; стратегията "преизчисляване" не се проваля при тези условия. Бих казал повече, но това ще разкрие трика във въпроса с трик, за който споменах по-горе.