SQL 定位连续值区间的开始值和结束值

SQL 定位连续值区间的开始值和结束值,本实例是本章第一个实例的引申,它们都用到了视图 V。在之前的实例中,你已经找到了包含一组连续值的区间,你希望知道它们的开始值和结束值。与本章的第一个实例不同,如果有一行并不属于某个连续值区间,你仍然希望返回它。为什么?这是因为,这样的行自成一个区间,它也有自己的开始值和结束值。

SQL 定位连续值区间的开始值和结束值 问题描述

本实例是本章第一个实例的引申,它们都用到了视图 V。在之前的实例中,你已经找到了包含一组连续值的区间,你希望知道它们的开始值和结束值。与本章的第一个实例不同,如果有一行并不属于某个连续值区间,你仍然希望返回它。为什么?这是因为,这样的行自成一个区间,它也有自己的开始值和结束值。视图 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_GRP PROJ_START  PROJ_END
-------- ----------- -----------
       1 01-JAN-2005 05-JAN-2005
       2 06-JAN-2005 07-JAN-2005
       3 16-JAN-2005 20-JAN-2005
       4 21-JAN-2005 22-JAN-2005
       5 26-JAN-2005 30-JAN-2005

SQL 定位连续值区间的开始值和结束值 解决方案

相较于本章第一个实例,本问题更为复杂。首先,必须明确什么是区间。PROJ_STARTPROJ_END 的值决定哪些行属于同一个区间。如果某一行的 PROJ_START 值等于上一行的 PROJ_END 值,那么该行就是“连续”的,或者说它属于某个组。如果某一行的 PROJ_START 值不等于上一行的 PROJ_END 值,并且它的 PROJ_END 值也不等于下一行的 PROJ_START 值,那么该行自身就构成了一个独立的组。识别出区间之后,还要对每个区间相关的行进行分组,并找出每一组的开始值和结束值。
我们来看看最终结果集的第一行数据。PROJ_START 值是视图 VPROJ_ID 1 对应的 PROJ_STARTPROJ_END 则是视图 VPROJ_ID 4 对应的 PROJ_ENDPROJ_ID 4 后面并没有再出现一个连续值,因此它作为这一组连续值的最后一个被纳入了第一组。
DB2、MySQLPostgreSQLSQL Server
这些数据库对应的解决方案需要用到视图 V2,以增强代码的可读性。视图 V2 的定义如下。

create view v2
as
select a.*,
       case
         when (
            select b.proj_id
              from V b
             where a.proj_start = b.proj_end
              )
              is not null then 0 else 1
       end as flag
  from V a

其结果集如下所示。

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

在视图 V2 的基础上得到的解决方案如下所示。首先,找出那些属于某个连续值区间的行,并为它们分组。然后,调用 MIN 函数和 MAX 函数找出每一组的开始值和结束值。

 1  select proj_grp,
 2         min(proj_start) as proj_start,
 3         max(proj_end) as proj_end
 4    from (
 5  select a.proj_id,a.proj_start,a.proj_end,
 6         (select sum(b.flag)
 7            from V2 b
 8           where b.proj_id <= a.proj_id) as proj_grp
 9    from V2 a
10         ) x
11   group by proj_grp

Oracle
上述解决方案当然也适用于 Oracle。不过,借助 Oracle 的窗口函数 LAG OVER,无须额外的视图也能解决本问题。我们可以利用 LAG OVER 函数判定前一行的 PROJ_END 是否等于当前行的 PROJ_START,并以此为标准对当前行进行分组。分组完成之后,接着调用聚合函数 MINMAX 分别找出每组的开始值和结束值。

 1  select proj_grp, min(proj_start), max(proj_end)
 2    from (
 3  select proj_id,proj_start,proj_end,
 4         sum(flag)over(order by proj_id) proj_grp
 5    from (
 6  select proj_id,proj_start,proj_end,
 7         case when
 8              lag(proj_end)over(order by proj_id) = proj_start
 9              then 0 else 1
10         end flag
11    from V
12         )
13         )
14   group by proj_grp

SQL 定位连续值区间的开始值和结束值 扩展知识

DB2、MySQLPostgreSQL 和 SQL Server
有了视图 V2,本问题就相对容易一些了。视图 V2CASE 表达式里用了一个标量子查询来判断当前行是否属于某个连续值区间。如果当前行属于某个连续值区间,那么别名为 FLAGCASE 表达式将返回 0;反之,则返回 1(判定当前行是否属于一组连续值区间的方法是:是否有一条记录的 PROJ_END 值等于当前行的 PROJ_START 值)。下一步是逐一查看内嵌视图 X(第 5 ~ 9 行)的查询结果。内嵌视图 X 返回视图 V2 的全部行以及针对 FLAG 的累计值,该累计值就是我们分组的依据,如下所示。

select a.proj_id,a.proj_start,a.proj_end,
       (select sum(b.flag)
          from v2 b
         where b.proj_id <= a.proj_id) as proj_grp
  from v2 a
 
PROJ_ID PROJ_START  PROJ_END      PROJ_GRP
------- ----------- ----------- ----------
      1 01-JAN-2005 02-JAN-2005          1
      2 02-JAN-2005 03-JAN-2005          1
      3 03-JAN-2005 04-JAN-2005          1
      4 04-JAN-2005 05-JAN-2005          1
      5 06-JAN-2005 07-JAN-2005          2
      6 16-JAN-2005 17-JAN-2005          3
      7 17-JAN-2005 18-JAN-2005          3
      8 18-JAN-2005 19-JAN-2005          3
      9 19-JAN-2005 20-JAN-2005          3
     10 21-JAN-2005 22-JAN-2005          4
     11 26-JAN-2005 27-JAN-2005          5
     12 27-JAN-2005 28-JAN-2005          5
     13 28-JAN-2005 29-JAN-2005          5
     14 29-JAN-2005 30-JAN-2005          5

现在已经确定好各个区间了,接下来要针对 PROJ_STARTPROJ_END 分别调用聚合函数 MINMAX 找出每个区间的开始值和结束值,然后根据上述累计值分组。
Oracle
对于本实例而言,窗口函数 LAG OVER 非常有用。我们无须使用自连接、标量子查询或额外的视图就能访问前一行的 PROJ_END 值。去掉了 CASE 表达式的 LAG OVER 函数的执行结果显示如下。

select proj_id,proj_start,proj_end,
       lag(proj_end)over(order by proj_id) prior_proj_end
  from V
 
PROJ_ID PROJ_START  PROJ_END    PRIOR_PROJ_END
------- ----------- ----------- --------------
      1 01-JAN-2005 02-JAN-2005
      2 02-JAN-2005 03-JAN-2005 02-JAN-2005
      3 03-JAN-2005 04-JAN-2005 03-JAN-2005
      4 04-JAN-2005 05-JAN-2005 04-JAN-2005
      5 06-JAN-2005 07-JAN-2005 05-JAN-2005
      6 16-JAN-2005 17-JAN-2005 07-JAN-2005
      7 17-JAN-2005 18-JAN-2005 17-JAN-2005
      8 18-JAN-2005 19-JAN-2005 18-JAN-2005
      9 19-JAN-2005 20-JAN-2005 19-JAN-2005
     10 21-JAN-2005 22-JAN-2005 20-JAN-2005
     11 26-JAN-2005 27-JAN-2005 22-JAN-2005
     12 27-JAN-2005 28-JAN-2005 27-JAN-2005
     13 28-JAN-2005 29-JAN-2005 28-JAN-2005
     14 29-JAN-2005 30-JAN-2005 29-JAN-2005

从完整的代码中可以看到,CASE 表达式只是比较了 LAG OVER 函数返回的结果和当前行的 PROJ_START 值;如果两个值相等,则返回 0,否则返回 1。下一步就是针对 CASE 表达式返回的 0 和 1 产生一个累计值,从而把每一行都编入某个组。上述累计值的计算结果显示如下。

select proj_id,proj_start,proj_end,
       sum(flag)over(order by proj_id) proj_grp
  from (
select proj_id,proj_start,proj_end,
       case when
            lag(proj_end)over(order by proj_id) = proj_start
            then 0 else 1
       end flag
  from V
       )
 
PROJ_ID PROJ_START  PROJ_END      PROJ_GRP
------- ----------- ----------- ----------
      1 01-JAN-2005 02-JAN-2005          1
      2 02-JAN-2005 03-JAN-2005          1
      3 03-JAN-2005 04-JAN-2005          1
      4 04-JAN-2005 05-JAN-2005          1
      5 06-JAN-2005 07-JAN-2005          2
      6 16-JAN-2005 17-JAN-2005          3
      7 17-JAN-2005 18-JAN-2005          3
      8 18-JAN-2005 19-JAN-2005          3
      9 19-JAN-2005 20-JAN-2005          3
     10 21-JAN-2005 22-JAN-2005          4
     11 26-JAN-2005 27-JAN-2005          5
     12 27-JAN-2005 28-JAN-2005          5
     13 28-JAN-2005 29-JAN-2005          5
     14 29-JAN-2005 30-JAN-2005          5

现在每一行都被放入了各自的组,只要针对 PROJ_START 值和 PROJ_END 值分别调用聚合函数 MINMAX,然后基于 PROJ_GRP 列的累计值分组即可。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程