SQL 定位连续的值区间

SQL 定位连续的值区间,你想确定哪些行代表了一系列在时间上连续的项目。考虑下述视图 V 的结果集,它包含了项目编号以及各自的起止日期。

SQL 定位连续的值区间 问题描述

你想确定哪些行代表了一系列在时间上连续的项目。考虑下述视图 V 的结果集,它包含了项目编号以及各自的起止日期。

select *
  from V
 
PROJ_ID PROJ_START  PROJ_END
------- ----------- -----------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005
      3 03-JAN-2005 04-JAN-2005
      4 04-JAN-2005 05-JAN-2005
      5 06-JAN-2005 07-JAN-2005
      6 16-JAN-2005 17-JAN-2005
      7 17-JAN-2005 18-JAN-2005
      8 18-JAN-2005 19-JAN-2005
      9 19-JAN-2005 20-JAN-2005
     10 21-JAN-2005 22-JAN-2005
     11 26-JAN-2005 27-JAN-2005
     12 27-JAN-2005 28-JAN-2005
     13 28-JAN-2005 29-JAN-2005
     14 29-JAN-2005 30-JAN-2005

除了第一行,其他每一行的 PROJ_START 应该等于前一行的 PROJ_END(“前一行”的定义是其 PROJ_ID 等于当前行的 PROJ_ID 减 1)。仔细查看视图 V 的前 5 行,PROJ_ID 分别等于 1 到 3 的行属于同一“组”,因为每一行的 PROJ_END 都等于后一行的 PROJ_START。我们希望找出一系列连续项目的日期区间,因此希望返回满足“当前行的 PROJ_END 等于下一行的 PROJ_START”这一条件的所有行。如果整个结果集只包含前 5 行,那么我们希望返回的只是最前面的 3 行。(对于视图 V 全部 14 行数据而言)最终的结果集应该如下所示。

PROJ_ID PROJ_START  PROJ_END
------- ----------- -----------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005
      3 03-JAN-2005 04-JAN-2005
      6 16-JAN-2005 17-JAN-2005
      7 17-JAN-2005 18-JAN-2005
      8 18-JAN-2005 19-JAN-2005
     11 26-JAN-2005 27-JAN-2005
     12 27-JAN-2005 28-JAN-2005
     13 28-JAN-2005 29-JAN-2005

我们从结果集中剔除掉了 PROJ_ID 为 4、5、9、10 和 14 的行,因为这些行的 PROJ_END 不等于下一行的 PROJ_START

SQL 定位连续的值区间 解决方案

DB2、MySQLPostgreSQLSQL Server
使用自连接找出包含连续值的行。

1 select v1.proj_id,
2        v1.proj_start,
3        v1.proj_end
4   from V v1, V v2
5  where v1.proj_end = v2.proj_start

Oracle
上述解决方案也适用于 Oracle。除此之外,Oracle 还有另一个解决方案,即利用窗口函数 LEAD OVER 去查看“下一行”的 BEGIN_DATE,这样就不必自连接了。

1 select proj_id,proj_start,proj_end
2   from (
3 select proj_id,proj_start,proj_end,
4        lead(proj_start)over(order by proj_id) next_proj_start
5   from V
6         )
7 where next_proj_start = proj_end

SQL 定位连续的值区间 扩展知识

DB2、MySQLPostgreSQL 和 SQL Server
通过视图 V 的自连接,每一行都可以与其他行进行比较。考虑查询 PROJ_ID 为 1 和 4 时得到的部分结果集。

select v1.proj_id as v1_id,
       v1.proj_end as v1_end,
       v2.proj_start as v2_begin,
       v2.proj_id as v2_id
  from v v1, v v2
 where v1.proj_id in ( 1,4 )
 
V1_ID V1_END      V2_BEGIN         V2_ID
----- ----------- ----------- ----------
    1 02-JAN-2005 01-JAN-2005          1
    1 02-JAN-2005 02-JAN-2005          2
    1 02-JAN-2005 03-JAN-2005          3
    1 02-JAN-2005 04-JAN-2005          4
    1 02-JAN-2005 06-JAN-2005          5
    1 02-JAN-2005 16-JAN-2005          6
    1 02-JAN-2005 17-JAN-2005          7
    1 02-JAN-2005 18-JAN-2005          8
    1 02-JAN-2005 19-JAN-2005          9
    1 02-JAN-2005 21-JAN-2005         10
    1 02-JAN-2005 26-JAN-2005         11
    1 02-JAN-2005 27-JAN-2005         12
    1 02-JAN-2005 28-JAN-2005         13
    1 02-JAN-2005 29-JAN-2005         14
    4 05-JAN-2005 01-JAN-2005          1
    4 05-JAN-2005 02-JAN-2005          2
    4 05-JAN-2005 03-JAN-2005          3
    4 05-JAN-2005 04-JAN-2005          4
    4 05-JAN-2005 06-JAN-2005          5
    4 05-JAN-2005 16-JAN-2005          6
    4 05-JAN-2005 17-JAN-2005          7
    4 05-JAN-2005 18-JAN-2005          8
    4 05-JAN-2005 19-JAN-2005          9
    4 05-JAN-2005 21-JAN-2005         10
    4 05-JAN-2005 26-JAN-2005         11
    4 05-JAN-2005 27-JAN-2005         12
    4 05-JAN-2005 28-JAN-2005         13
    4 05-JAN-2005 29-JAN-2005         14

仔细检查以上结果集,我们就会明白为什么 PROJ_ID 1 被包含在最终结果集中,而 PROJ_ID4 却没有。这是因为对于 V1_ID 4 而言,不存在与 V1_END 值相等的 V2_BEGIN
如果改变一下筛选条件,PROJ_ID 4 也可以被认为是相邻的项目。考虑如下所示的结果集。

select *
  from V
 where proj_id <= 5
 
PROJ_ID PROJ_START  PROJ_END
------- ----------- -----------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005
      3 03-JAN-2005 04-JAN-2005
      4 04-JAN-2005 05-JAN-2005
      5 06-JAN-2005 07-JAN-2005

如果“相邻的项目”是指其开始日期与另一个项目的结束日期相同,那么 PROJ_ID 4 也应该被包含在结果集里。按照最初的筛选条件,由于前向比较(比较 PROJ_END 和下一行的 PROJ_START)的存在,PROJ_ID 4 被排除在外了,但如果做后向比较(比较 PROJ_START 和前一行的 PROJ_END),那么 PROJ_ID 4 就应该被包含在结果集中。
不妨修改一下上述解决方案,把 PROJ_ID 4 也包含进去:只需增加一个筛选条件,确保 PROJ_STARTPROJ_END 的相邻关系都被纳入检查范围就行了,而不是仅仅检查 PROJ_END。下面的查询展示了上述改动,该查询产生了一个包含 PROJ_ID 4 的结果集(DISTINCT 是必要的,因为一些行同时满足两个条件)。

select distinct
       v1.proj_id,
       v1.proj_start,
       v1.proj_end
  from V v1, V v2
 where v1.proj_end   = v2.proj_start
    or v1.proj_start = v2.proj_end
 
PROJ_ID PROJ_START  PROJ_END
------ ----------- -----------
     1 01-JAN-2005 02-JAN-2005
     2 02-JAN-2005 03-JAN-2005
     3 03-JAN-2005 04-JAN-2005
     4 04-JAN-2005 05-JAN-2005

Oracle
上述自连接方案当然适用于本问题,不过窗口函数 LEAD OVER 用于解决这一类问题更合适。LEAD OVER 函数可以不执行自连接就能查看其他行的数据(尽管该函数要求必须对结果集进行排序)。对于 PROJ_ID1PROJ_ID4,考虑内嵌视图(第 3 ~ 5 行)的结果集。

select *
  from (
select proj_id,proj_start,proj_end,
       lead(proj_start)over(order by proj_id) next_proj_start
  from v
       )
 where proj_id in ( 1,4 )
 
PROJ_ID PROJ_START  PROJ_END    NEXT_PROJ_START
------- ----------- ----------- ---------------
      1 01-JAN-2005 02-JAN-2005 02-JAN-2005
      4 04-JAN-2005 05-JAN-2005 06-JAN-2005

仔细检查上面的代码片段及其结果集,不难理解为什么 PROJ_ID 4 会被从最终结果集里排除掉。这是因为 PROJ_END 的日期 2005 年 1 月 5 日与下一个项目的开始日期 2005 年 1 月 6 日不同。
LEAD OVER 函数用于解决这一类问题非常方便,尤其在只遍历部分结果的时候。使用窗口函数时,要记得它们在 FROMWHERE 子句之后才会被评估,因此前面的查询里的 LEAD OVER 函数必须被放入一个内嵌视图才行。否则,LEAD OVER 函数的作用对象就变成了只含有 PROJ_ID 1PROJ_ID 4 的结果集,因为执行过 WHERE 子句之后,其他行都会被过滤掉。
现在,如果改变一下筛选条件,我们也能把 PROJ_ID 4 纳入最终的结果集。考虑视图 V 的前 5 行数据。

select *
  from V
 where proj_id <= 5
 
PROJ_ID PROJ_START  PROJ_END
------- ----------- -----------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005
      3 03-JAN-2005 04-JAN-2005
      4 04-JAN-2005 05-JAN-2005
      5 06-JAN-2005 07-JAN-2005

如果我们认为 PROJ_ID 4 事实上是相邻的(因为 PROJ_ID 4PROJ_START 等于 PROJ_ID 3PROJ_END),而且只有 PROJ_ID 5 应该被排除在外,那么前述解决方案就不正确了,至少是不全面的。

select proj_id,proj_start,proj_end
  from (
select proj_id,proj_start,proj_end,
       lead(proj_start)over(order by proj_id) next_start
  from V
 where proj_id <= 5
       )
 where proj_end = next_start
 
PROJ_ID PROJ_START  PROJ_END
------- ----------- -----------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005
      3 03-JAN-2005 04-JAN-2005

如果我们认为 PROJ_ID 4 应该被包含进来,只需额外加上 LAG OVER 函数,并在 WHERE 子句中也增加一个过滤条件即可。

select proj_id,proj_start,proj_end
  from (
select proj_id,proj_start,proj_end,
       lead(proj_start)over(order by proj_id) next_start,
       lag(proj_end)over(order by proj_id) last_end
  from V
 where proj_id <= 5
       )
 where proj_end   = next_start
    or proj_start = last_end
 
PROJ_ID PROJ_START  PROJ_END
------- ----------- -----------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005
      3 03-JAN-2005 04-JAN-2005
      4 04-JAN-2005 05-JAN-2005

这样一来,PROJ_ID 4 也被纳入最终结果集了,只有 PROJ_ID 5 会被排除在外。当然,如果真的要对本解决方案做出上述代码改动,一定仔细确认需求。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程