我有一个具有以下结构的表:ID,Month,Year,Value,每个id每月一个条目的值,大多数月份具有相同的值。
我想为该表创建一个视图,该视图折叠相同的值,如下所示:ID,开始月,结束月,开始年份,结束年份,值,每个值每个值一行。
问题是,如果值更改然后返回到原始值,则表中应该有两行
所以:
应该产生
以下查询适用于除此特殊情况之外的所有内容,当值返回原始值时。
select distinct id, min(month) keep (dense_rank first order by month)
over (partition by id, value) startMonth,
max(month) keep (dense_rank first order by month desc) over (partition
by id, value) endMonth,
value
数据库是Oracle
答案 0 :(得分:47)
我将逐步开发我的解决方案,将每个转换分解为一个视图。这有助于解释正在做什么,并有助于调试和测试。它实质上是将功能分解原理应用于数据库查询。
我也将在不使用Oracle扩展的情况下执行此操作,SQL应该在任何现代RBDMS上运行。所以没有保持,覆盖,分区,只是子查询和分组。 (如果它在您的RDBMS上不起作用,请在评论中通知我。)
首先,表格,因为我没有创造力,所以我会调用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发生变化。在表格中没有月份的情况下,它就是空的。
由于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)我们的数据中没有下个月没有空值;为了达到OP的规范,那些应该是单月范围。
编辑:这些实际上可能比一个月更长的范围,但在每种情况下,他们都意味着我们需要找到最新的月份,这是:(select max(am) from cm_abs_month d where d.eid = a.eid )
因为视图会分解问题,所以我们可以通过添加另一个视图来添加此月份的“结束时间”,但我只是将其插入到coalesce中。哪个最有效取决于您的RDBMS如何优化查询。
要获得一个月前,我们将加入(cm_result_data.last_am - 1 = cm_abs_month.am)
只要我们有null,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在评论中提问,
如果有,您的查询会发生什么 是数据中的差距(比如说有一个 输入2007-12,价值80,和 2007-10的另一个,但不是一个 2007-11?问题不明确是什么 应该发生在那里。
嗯,你是完全正确的,OP没有指明。也许存在(未提及的)前提条件,即没有间隙。在没有要求的情况下,我们不应该尝试编写可能不存在的内容。但事实是,差距使得“加入”战略失败;在这些条件下,“重新计算”策略不会失败。我会说更多,但这将揭示我在上面提到的技巧问题中的诀窍。
答案 1 :(得分:1)
我按照以下方式开展工作。它对分析函数很重要,并且是特定于Oracle的。
select distinct id, value,
decode(startMonth, null,
lag(startMonth) over(partition by id, value order by startMonth, endMonth), --if start is null, it's an end so take from the row before
startMonth) startMonth,
decode(endMonth, null,
lead(endMonth) over(partition by id, value order by startMonth, endMonth), --if end is null, it's an start so take from the row after
endMonth) endMonth
from (
select id, value, startMonth, endMonth from(
select id, value,
decode(month+1, lead(month) over(partition by id,value order by month), null, month)
startMonth, --get the beginning month for each interval
decode(month-1, lag(month) over(partition by id,value order by month), null, month)
endMonth --get the end month for each interval from Tbl
) a
where startMonth is not null or endMonth is not null --remain with start and ends only
)b
有可能在某种程度上简化一些内部查询
内部查询检查月份是否是间隔的第一个/最后一个月,如下:如果月份+ 1 = =下一个月(滞后)该分组,那么由于下个月,本月是显然不是结束月份。否则, 是间隔的最后一个月。相同的概念用于检查第一个月。
外部查询首先筛选出不是开始月份或结束月份(where startMonth is not null or endMonth is not null
)的所有行。
然后,每行是开始月份或结束月份(或两者),由start或end是否为null确定。如果月份是开始月份,则获取相应的结束月份,获取该id的下一个(前导)endMonth,endMonth排序的值,如果是endMonth,则通过查找前一个startMonth(滞后)来获取startMonth
答案 2 :(得分:1)
这个只使用一次表扫描并且可以使用多年。最好将月份和年份列建模为仅一个日期数据类型列:
SQL> create table tbl (id,month,year,value)
2 as
3 select 100,12,2007,80 from dual union all
4 select 100,1,2008,80 from dual union all
5 select 100,2,2008,80 from dual union all
6 select 100,3,2008,90 from dual union all
7 select 100,4,2008,80 from dual union all
8 select 200,12,2007,50 from dual union all
9 select 200,1,2008,50 from dual union all
10 select 200,2,2008,40 from dual union all
11 select 200,3,2008,50 from dual union all
12 select 200,4,2008,50 from dual union all
13 select 200,5,2008,50 from dual
14 /
Tabel is aangemaakt.
SQL> select id
2 , mod(min(year*12+month-1),12)+1 startmonth
3 , trunc(min(year*12+month-1)/12) startyear
4 , mod(max(year*12+month-1),12)+1 endmonth
5 , trunc(max(year*12+month-1)/12) endyear
6 , value
7 from ( select id
8 , month
9 , year
10 , value
11 , max(rn) over (partition by id order by year,month) maxrn
12 from ( select id
13 , month
14 , year
15 , value
16 , case lag(value) over (partition by id order by year,month)
17 when value then null
18 else rownum
19 end rn
20 from tbl
21 ) inner
22 )
23 group by id
24 , maxrn
25 , value
26 order by id
27 , startyear
28 , startmonth
29 /
ID STARTMONTH STARTYEAR ENDMONTH ENDYEAR VALUE
---------- ---------- ---------- ---------- ---------- ----------
100 12 2007 2 2008 80
100 3 2008 3 2008 90
100 4 2008 4 2008 80
200 12 2007 1 2008 50
200 2 2008 2 2008 40
200 3 2008 5 2008 50
6 rijen zijn geselecteerd.
此致 罗布。
答案 3 :(得分:0)
当输入表包含多个ID和多年的日期范围时,我无法从ngz获得响应。我有一个有效的解决方案,但有资格。如果您知道该范围内的每个月/年/ ID组合都有一行,它只会给您正确的答案。如果有“漏洞”则不起作用。如果你有洞,我知道除了编写一些PL / SQL并使用游标循环以你想要的格式创建一个新表之外,我知道这样做很好。
顺便说一下,这就是为什么以这种方式建模的数据是令人厌恶的。您应始终将内容存储为起始/来自范围记录,而不是作为离散时间段记录。使用“乘数”表将前者转换为后者是微不足道的,但是几乎不可能(正如你所见)走向另一个方向。
SELECT ID
, VALUE
, start_date
, end_date
FROM (SELECT ID
, VALUE
, start_date
, CASE
WHEN is_last = 0
THEN LEAD(end_date) OVER(PARTITION BY ID ORDER BY start_date)
ELSE end_date
END end_date
, is_first
FROM (SELECT ID
, VALUE
, TO_CHAR(the_date, 'YYYY.MM') start_date
, TO_CHAR(NVL(LEAD(the_date - 31) OVER(PARTITION BY ID ORDER BY YEAR
, MONTH), the_date), 'YYYY.MM') end_date
, is_first
, is_last
FROM (SELECT ID
, YEAR
, MONTH
, TO_DATE(TO_CHAR(YEAR) || '.' || TO_CHAR(MONTH) || '.' || '15', 'YYYY.MM.DD') the_date
, VALUE
, ABS(SIGN(VALUE -(NVL(LAG(VALUE) OVER(PARTITION BY ID ORDER BY YEAR
, MONTH), VALUE - 1)))) is_first
, ABS(SIGN(VALUE -(NVL(LEAD(VALUE) OVER(PARTITION BY ID ORDER BY YEAR
, MONTH), VALUE - 1)))) is_last
FROM test_table)
WHERE is_first = 1
OR is_last = 1))
WHERE is_first = 1