Я собираюсь разрабатывать свое решение постепенно, раскладывая каждое преобразование в представление. Это помогает объяснить, что делается, и помогает в отладке и тестировании. По сути, он применяет принцип функциональной декомпозиции к запросам к базе данных.
Я также собираюсь сделать это без использования расширений Oracle, с SQL, который должен работать на любой современной RBDMS. Таким образом, нет хранения, разделения, просто подзапросы и групповые байсы. (Сообщите мне в комментариях, если это не работает на вашей РСУБД.)
Во-первых, таблица, которую я не знаю, я назову month_value. Поскольку идентификатор на самом деле не является уникальным идентификатором, я назову его «Ид». Другими столбцами являются «m», «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. Все, чего не хватает, это что-то, чтобы преобразовать абсолютные месяцы обратно в (у, м) кортежи.
Для этого мы просто присоединимся к таблице month_value.
Есть только две проблемы:
1) мы хотим, чтобы месяц до last_am в нашем выводе, и
2) у нас есть нули, которых нет в следующем месяце в наших данных; чтобы соответствовать спецификации OP, это должны быть месячные диапазоны.
РЕДАКТИРОВАТЬ: На самом деле это могут быть более длинные диапазоны, чем один месяц, но в каждом случае они означают, что мы должны найти самый последний месяц для ИД, который является:
(select max(am) from cm_abs_month d where d.eid = a.eid )
Поскольку представления разлагают проблему, мы могли бы добавить эту «заглушку» месяцем ранее, добавив другое представление, но я просто вставлю это в объединение. Что будет наиболее эффективным, зависит от того, как ваша СУБД оптимизирует запросы.
Чтобы получить месяц раньше, мы присоединимся (cm_result_data.last_am - 1 = cm_abs_month.am)
В тех случаях, когда у нас есть ноль, OP хочет, чтобы месяц "до" был таким же, как месяц "с", поэтому мы просто используем для этого 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;
И теперь мы получаем данные, отформатированные в соответствии с ОП:
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, которое должно работать в любой СУБД и разбито на простые, понятные и легко тестируемые представления.
Лучше ли присоединиться или пересчитать? Я оставлю это (это вопрос с подвохом) читателю.
(Если ваша СУБД не разрешает групповые байсы в представлениях, вам нужно сначала присоединиться, а затем к группе или группе, а затем указать месяц и год с соответствующими подзапросами. Это оставлено в качестве упражнения для читателя .)
Джонатан Леффлер спрашивает в комментариях,
Что произойдет с вашим запросом, если есть
пробелы в данных (скажем, есть
запись для 2007-12 со значением 80, и
другой на 2007-10, но не один для
2007-11? Вопрос не ясно, что
должно произойти там.
Ну, вы совершенно правы, ОП не указывает. Возможно, есть (не упомянутое) предварительное условие, что нет пробелов. В отсутствие требования мы не должны пытаться кодировать что-то, чего может не быть. Но факт в том, что из-за пробелов стратегия «присоединения назад» терпит неудачу; стратегия пересчета не терпит неудачу в этих условиях. Я бы сказал больше, но это раскрыло бы уловку в вопросе об уловке, на который я ссылался выше.