Vou desenvolver minha solução de forma incremental, decompondo cada transformação em uma visão. Isso ajuda a explicar o que está sendo feito e ajuda na depuração e teste. É essencialmente aplicar o princípio da decomposição funcional às consultas de banco de dados.
Também vou fazer isso sem usar extensões Oracle, com SQL que deve rodar em qualquer RBDMS moderno. Portanto, não há keep, over, partition, apenas subconsultas e group bys. (Informe-me nos comentários se não funcionar no seu RDBMS.)
Primeiro, a tabela, que como não sou criativo, vou chamar valor_meses. Como o id não é realmente um id exclusivo, vou chamá-lo de "eid". As outras colunas são "m"onth, "y"ear e "v"alue:
create table month_value(
eid int not null, m int, y int, v int );
Após inserir os dados, para dois eids, tenho:
> 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)
Em seguida, temos uma entidade, o mês, que é representada como duas variáveis. Isso deve ser realmente uma coluna (ou uma data ou um datetime, ou talvez até mesmo uma chave estrangeira para uma tabela de datas), então faremos uma coluna. Faremos isso como uma transformação linear, de modo que seja classificado da mesma forma que (y, m), e de modo que para qualquer tupla (y,m) haja um e único valor, e todos os valores sejam consecutivos:
> create view cm_abs_month as
select *, y * 12 + m as am from month_value;
Isso nos dá:
> 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)
Agora usaremos uma autojunção em uma subconsulta correlacionada para encontrar, para cada linha, o primeiro mês sucessor em que o valor muda. Basearemos essa visualização na visualização anterior que criamos:
> 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 agora é o "mês absoluto" do primeiro mês (mais cedo) (após o mês da linha atual) em que o valor, v, muda. É nulo onde não há mês posterior, para esse eid, na tabela.
Como last_am é o mesmo para todos os meses que antecedem a mudança em v (que ocorre em last_am), podemos agrupar em last_am e v (e eid, é claro), e em qualquer grupo, o min(am) é o valor absoluto mês do primeiro mês consecutivo que teve esse valor:
> 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)
Agora, este é o conjunto de resultados que queremos, e é por isso que essa visualização é chamada cm_result_data. Tudo o que falta é algo para transformar meses absolutos de volta em tuplas (y,m).
Para fazer isso, vamos apenas juntar à tabela month_value.
Existem apenas dois problemas:1) queremos o mês antes last_am em nossa saída e2) temos nulos onde não há próximo mês em nossos dados; para atender a especificação do OP, esses devem ser intervalos de um mês.
EDIT:Estes podem ser intervalos maiores que um mês, mas em todos os casos eles significam que precisamos encontrar o último mês para o eid, que é:
(select max(am) from cm_abs_month d where d.eid = a.eid )
Como as visualizações decompõem o problema, poderíamos adicionar esse "limite final" no mês anterior, adicionando outra visualização, mas apenas inserirei isso no coalesce. O que seria mais eficiente depende de como seu RDBMS otimiza as consultas.
Para chegar um mês antes, vamos aderir (cm_result_data.last_am - 1 =cm_abs_month.am)
Onde quer que tenhamos um nulo, o OP quer que o mês "para" seja o mesmo que o mês "de", então usaremos apenas coalesce nisso:coalesce( last_am, am). Como last elimina quaisquer nulos, nossas junções não precisam ser junções externas.
> 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 |
+-----+------+------+------+------+------+
Ao juntar-se novamente, obtemos a saída que o OP deseja.
Não que tenhamos que nos juntar de volta. Acontece que nossa função absolute_month é bidirecional, então podemos apenas recalcular o ano e compensar o mês a partir dela.
Primeiro, vamos adicionar o mês "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;
E agora obtemos os dados, formatados de acordo com o 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 |
+-----+------+------+------+------+------+
E há os dados que o OP deseja. Tudo em SQL que deve rodar em qualquer RDBMS, e é decomposto em views simples, fáceis de entender e fáceis de testar.
É melhor voltar ou recalcular? Vou deixar isso (é uma pergunta capciosa) para o leitor.
(Se o seu RDBMS não permitir group bys nas visualizações, você terá que ingressar primeiro e depois agrupar, ou agrupar e depois puxar o mês e o ano com subconsultas correlacionadas. Isso fica como exercício para o leitor.)
Jonathan Leffler pergunta nos comentários,
O que acontece com sua consulta se houver lacunas nos dados (digamos que haja uma entrada para 2007-12 com valor 80 e outra para 2007-10, mas não uma para 2007-11? A questão não é clara o que deve acontecer lá.
Bem, você está exatamente certo, o OP não especifica. Talvez haja uma pré-condição (não mencionada) de que não haja lacunas. Na ausência de um requisito, não devemos tentar codificar algo que pode não estar lá. Mas, o fato é que as lacunas fazem com que a estratégia de "reunir-se" falhe; a estratégia "recalcular" não falha nessas condições. Eu diria mais, mas isso revelaria o truque na pergunta capciosa que aludi acima.