SQL查询按日期范围折叠重复值

时间:2009-03-31 18:44:01

标签: sql oracle

我有一个具有以下结构的表:ID,Month,Year,Value,每个id每月一个条目的值,大多数月份具有相同的值。

我想为该表创建一个视图,该视图折叠相同的值,如下所示:ID,开始月,结束月,开始年份,结束年份,值,每个值每个值一行。

问题是,如果值更改然后返回到原始值,则表中应该有两行

所以:

  • 100 1 2008 80
  • 100 2 2008 80
  • 100 3 2008 90
  • 100 4 2008 80

应该产生

  • 100 1 2008 2 2008 80
  • 100 3 2008 3 2008 90
  • 100 4 2008 4 2008 80

以下查询适用于除此特殊情况之外的所有内容,当值返回原始值时。

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

4 个答案:

答案 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