我将逐步开发我的解决方案,将每个转换分解为一个视图。这既有助于解释正在做什么,也有助于调试和测试。它本质上是将功能分解的原理应用于数据库查询。
我也打算不使用 Oracle 扩展来完成它,使用应该在任何现代 RBDMS 上运行的 SQL。所以没有保留,过度,分区,只有子查询和分组。 (如果它不适用于您的 RDBMS,请在 cmets 中通知我。)
首先是表格,由于我没有创意,我将其称为month_value。由于 id 实际上不是唯一的 id,我将其称为“eid”。其他列是“m”onth、“y”ear 和“v”alue:
create table month_value(
eid int not null, m int, y int, v int );
插入数据后,对于两个 eid,我有:
> 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 的较晚月份的情况下为 null。
由于在 v 发生变化(发生在 last_am)之前的所有月份中 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) 我们希望输出中的月份 before last_am,并且
2)我们的数据中没有下个月的地方有空值;为了满足 OP 的规范,这些应该是单月范围。
编辑:这些实际上可能比一个月更长,但在每种情况下,它们都意味着我们需要找到开斋节的最新月份,即:
(select max(am) from cm_abs_month d where d.eid = a.eid )
因为视图分解了问题,我们可以在一个月前通过添加另一个视图来添加这个“结束上限”,但我只是将它插入到合并中。哪个最有效取决于您的 RDBMS 如何优化查询。
要获得前一个月,我们将加入 (cm_result_data.last_am - 1 = cm_abs_month.am)
只要我们有一个空值,OP 希望“to”月份与“from”月份相同,因此我们将只使用 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 |
+-----+------+------+------+------+------+
还有 OP 想要的数据。一切都在 SQL 中,应该在任何 RDBMS 上运行,并被分解为简单、易于理解和易于测试的视图。
重新加入好还是重新计算好?我会把这个(这是一个棘手的问题)留给读者。
(如果您的 RDBMS 不允许在视图中进行分组,则您必须先加入,然后再分组,或者分组,然后使用相关的子查询拉入月份和年份。这留给读者练习.)
Jonathan Leffler 在 cmets 中提问,
如果存在,您的查询会发生什么
是数据中的空白(比如说有一个
2007-12 年的条目,值为 80,和
2007-10 年的另一个,但没有一个
2007-11 年?问题不清楚是什么
应该发生在那里。
嗯,你是对的,OP 没有具体说明。也许有一个(未提及的)前提条件是没有差距。在没有要求的情况下,我们不应该尝试围绕可能不存在的东西进行编码。但事实是,差距导致“回归”战略失败;在这些条件下,“重新计算”策略不会失败。我会说更多,但这会揭示我上面提到的技巧问题中的技巧。