涉及区间的查询
1、找出一系列连续的值
问题:你想判断哪些行表示一系列连续的项目。请看下面从视图V 返回的结果集,它包含一系列项目及其开始日期和结束日期。
sql
select *
from V
PROJ_ID PROJ_START PROJ_END
------- ----------- -----------
1 01-JAN-2020 02-JAN-2020
2 02-JAN-2020 03-JAN-2020
3 03-JAN-2020 04-JAN-2020
4 04-JAN-2020 05-JAN-2020
5 06-JAN-2020 07-JAN-2020
6 16-JAN-2020 17-JAN-2020
7 17-JAN-2020 18-JAN-2020
8 18-JAN-2020 19-JAN-2020
9 19-JAN-2020 20-JAN-2020
10 21-JAN-2020 22-JAN-2020
11 26-JAN-2020 27-JAN-2020
12 27-JAN-2020 28-JAN-2020
13 28-JAN-2020 29-JAN-2020
14 29-JAN-2020 30-JAN-2020
所谓表示一系列连续项目的行,指的是除第一行外,其他各行的 PROJ_START 都与其前一行的 PROJ_END 相等(这里的前一行,指的是 PROJ_ID 比当前行小 1 的行)。在视图 V 返回的前 5 行中,PROJ_ID 为 1~3 的行属于同一"组",因为其中每行的 PROJ_END 都与下一行的 PROJ_START 相等。由于你要找出一系列连续项目所在的日期范围,因此要返回所有其 PROJ_END 与下一行的 PROJ_START 相等的行。如果整个结果集只包含前5 行,那么你要返回的是前 3 行。最终的结果集如下所示(以视图 V 中的全部 14 行为例)。
sql
PROJ_ID PROJ_START PROJ_END
------- ----------- -----------
1 01-JAN-2020 02-JAN-2020
2 02-JAN-2020 03-JAN-2020
3 03-JAN-2020 04-JAN-2020
6 16-JAN-2020 17-JAN-2020
7 17-JAN-2020 18-JAN-2020
8 18-JAN-2020 19-JAN-2020
11 26-JAN-2020 27-JAN-2020
12 27-JAN-2020 28-JAN-2020
13 28-JAN-2020 29-JAN-2020
上述结果集中没有包含 PROJ_ID 为 4、5、9、10 和 14的行,因为这些行的 PROJ_END 与下一行的PROJ_START 都不相等。
解决方案:本解决方案利用窗口函数 LEAD OVER 来查找下一行的BEGIN_DATE,从而避免使用自连接。而在窗口函数被引入前,必须使用自连接。
sql
select proj_id, proj_start, proj_end
from (
select proj_id, proj_start, proj_end,
lead(proj_start)over(order by proj_id) next_proj_start
from V
) alias
where next_proj_start = proj_end;
2、找出同一个分组或分区中相邻行的差
问题:你想返回每位员工的 DEPTNO、ENAME、SAL 及其与当前部门中(DEPTNO 相同)下一位员工的 SAL 差(以判断在同一部门中,资历和薪水之间是否存在相关性)。这里的下一位员工是根据获聘时间确定的。对于每个部门最后获聘的员工,将 SAL 差设置为 N/A。结果集如下所示。
sql
DEPTNO ENAME SAL HIREDATE DIFF
------ ---------- ---------- ----------- ----------
10 CLARK 2450 09-JUN-2006 -2550
10 KING 5000 17-NOV-2006 3700
10 MILLER 1300 23-JAN-2007 N/A
20 SMITH 800 17-DEC-2005 -2175
20 JONES 2975 02-APR-2006 -25
20 FORD 3000 03-DEC-2006 0
20 SCOTT 3000 09-DEC-2007 1900
20 ADAMS 1100 12-JAN-2008 N/A
30 ALLEN 1600 20-FEB-2006 350
30 WARD 1250 22-FEB-2006 -1600
30 BLAKE 2850 01-MAY-2006 1350
30 TURNER 1500 08-SEP-2006 250
30 MARTIN 1250 28-SEP-2006 300
30 JAMES 950 03-DEC-2006 N/A
解决方案:在本实例中,窗口函数 LEAD OVER 和 LAG OVER 又一次提供了极大的方便。无须使用连接,就可以轻松地访问下一行数据和前一行数据。对于这种问题,也可以使用其他方法(比如子查询或自连接)来解决,但显得比较笨拙。
sql
with next_sal_tab (deptno,ename,sal,hiredate,next_sal)
as
(select deptno, ename, sal, hiredate,
lead(sal)over(partition by deptno
order by hiredate) as next_sal
from emp )
select deptno, ename, sal, hiredate
, coalesce(cast(sal-next_sal as char), 'N/A') as diff
from next_sal_tab;
这里为展示解决方案的多样性,使用的是 CTE 而不是子查询。当前,在大多数 RDBMS 中,这两种方法都管用,具体选择哪种方法,通常取决于可读性。
3、找出连续值构成的区间的起点和终点
问题:本实例是 10.1 节的扩展,也使用 10.1 节中的视图 V。在10.1 节中,你找出了连续值构成的区间,而这里只想找出区间的起点和终点。与 10.1 节不同,在这个实例中,如果某行并非一组连续值的一部分,则你依然要返回它。为什么呢?这是因为这样的行是其自身构成的区间的起点和终点。这里将下面的视图 V 作为数据源。
sql
select *
from V
PROJ_ID PROJ_START PROJ_END
------- ----------- -----------
1 01-JAN-2020 02-JAN-2020
2 02-JAN-2020 03-JAN-2020
3 03-JAN-2020 04-JAN-2020
4 04-JAN-2020 05-JAN-2020
5 06-JAN-2020 07-JAN-2020
6 16-JAN-2020 17-JAN-2020
7 17-JAN-2020 18-JAN-2020
8 18-JAN-2020 19-JAN-2020
9 19-JAN-2020 20-JAN-2020
10 21-JAN-2020 22-JAN-2020
11 26-JAN-2020 27-JAN-2020
12 27-JAN-2020 28-JAN-2020
13 28-JAN-2020 29-JAN-2020
14 29-JAN-2020 30-JAN-2020
而你希望最终的结果集如下所示。
sql
PROJ_GRP PROJ_START PROJ_END
-------- ----------- -----------
1 01-JAN-2020 05-JAN-2020
2 06-JAN-2020 07-JAN-2020
3 16-JAN-2020 20-JAN-2020
4 21-JAN-2020 22-JAN-2020
5 26-JAN-2020 30-JAN-2020
解决方案:这个问题比 1 节的问题要复杂些。首先,必须找出所有的区间。一系列行是否构成区间取决于它们的PROJ_START 值和 PROJ_END 值。对于特定的行,仅当其 PROJ_START 值与前一行的 PROJ_END 值相等时,才被视为"连续"的,即属于分组的一部分。如果一行的PROJ_START 值与前一行的 PROJ_END 值不相等,且其PROJ_END 值与后一行的 PROJ_START 值不相等,则它自己构成一个分组。找出所有的区间后,必须将各个区间中的行编组,进而只返回区间的起点和终点。
请看前面要返回的最终结果集的第 1 行,其PROJ_START 为视图 V 中 PROJ_ID 1 的PROJ_START,而 PROJ_END 为视图 V 中 PROJ_ID 4的 PROJ_END。虽然 PROJ_ID 4 后面的值不是连续的,但它是连续值区间的终点,因此包含在第 1 个分组中。
对于这个问题,最简单的解决方法是使用窗口函数 LAGOVER。使用 LAG OVER 来判断当前行的 PROJ_START是否与前一行的 PROJ_END 相等,可以将所有的行分成多组。将行分组后,可以使用聚合函数 MIN 和 MAX 找出各个分组的起点和终点。
sql
select proj_grp, min(proj_start), max(proj_end)
from (
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
) alias1
) alias2
group by proj_grp;
4、填补值区间空隙
问题:你想返回从 2005 年起的 10 年间,每一年聘请的员工数量,但其中有些年份并没有聘请任何员工。你希望返回如下结果集。
sql
YR CNT
---- ----------
2005 1
2006 10
2007 2
2008 1
2009 0
2010 0
2011 0
2012 0
2013 0
2014 0
解决方案:本解决方案的诀窍是,对于没有聘请任何员工的年份,返回 0。如果某年没有聘请任何员工,那么在 EMP 表中,将没有获聘日期属于该年份的行。既然表中没有获聘日期属于该年份的行,那么怎么返回该年份聘请的员工数量呢?为了解决这个问题,必须使用外连接。你必须提供一个包含所有年份的结果集,然后对 EMP 表执行计数操作,看看各个年份是否聘请了员工。
DB2:将 EMP 表用作透视表(因为它包含 14 行数据),并使用内置函数 YEAR 生成 10 行数据,从 2005 年开始的 10年间的每一年对应一行。外连接到 EMP 表,并计算每年聘请的员工数量。
sql
select x.yr, coalesce(y.cnt,0) cnt
from (
select year(min(hiredate)over()) -
mod(year(min(hiredate)over()),10) +
row_number()over()-1 yr
from emp fetch first 10 rows only
) x
left join
(
select year(hiredate) yr1, count(*) cnt
from emp
group by year(hiredate)
) y
on ( x.yr = y.yr1 );
Oracle:Oracle 解决方案的结构与 DB2 解决方案相同,唯一不同的是语法。
sql
select x.yr, coalesce(cnt,0) cnt
from (
select extract(year from min(hiredate)over()) -
mod(extract(year from min(hiredate)over()),10) +
rownum-1 yr
from emp
where rownum <= 10
) x
left join
(
select to_number(to_char(hiredate,'YYYY')) yr, count(*) cnt
from emp
group by to_number(to_char(hiredate,'YYYY'))
) y
on ( x.yr = y.yr );
PostgreSQL 和 MySQL:将 T10 表作为透视表(因为它包含 10 行数据),并使用内置函数 EXTRACT 生成 10 行数据,从 2005 年开始的10 年间的每一年对应一行。外连接到 EMP 表,并计算每年聘请的员工数量。
sql
select y.yr, coalesce(x.cnt,0) as cnt
from (
selectmin_year-mod(cast(min_year as int),10)+rn as yr
from (
select (select min(extract(year from hiredate))
from emp) as min_year,
id-1 as rn
from t10
) a
) y
left join
(
select extract(year from hiredate) as yr, count(*) as cnt
from emp
group by extract(year from hiredate)
) x
on ( y.yr = x.yr );
SQL Server:将 EMP 表用作透视表(因为它包含 14 行数据),并使用内置函数 YEAR 生成 10 行数据,从 2005 年开始的 10年间的每一年对应一行。外连接到 EMP 表,并计算每年聘请的员工数量。
sql
select x.yr, coalesce(y.cnt,0) cnt
from (
select top (10)
(year(min(hiredate)over()) -
year(min(hiredate)over())%10)+
row_number()over(order by hiredate)-1 yr
from emp
) x
left join
(
select year(hiredate) yr, count(*) cnt
from emp
group by year(hiredate)
) y
on ( x.yr = y.yr );
5、生成连续的数字值
问题:你希望有一个"行源生成器"(row source generator),以便在查询中使用它。对于需要透视的查询,行源生成器很有用。例如,你想返回如下结果集,它包含指定的行数。
sql
ID
---
1
2
3
4
5
6
7
8
9
10
...
如果 RDBMS 提供了动态返回行的内置函数,就不用预先创建包含固定行数的透视表,这就是动态行生成器可以提供极大方便的原因。如果 RDBMS 没有提供这样的函数,你就必须在需要时使用包含固定行数的传统透视表来生成行(这可能还不够)。
解决方案:本解决方案演示如何返回 10 行数据,其中包含的数字从1 开始不断递增。你可以轻松地修改本解决方案,以返回任意的行数。
能够返回不断递增的值,这种能力打开了通往众多其他解决方案的大门。例如,你可以生成一系列数字,再将它们与特定日期相加,以生成日期序列。你还可以使用这些数字来分析字符串。
DB2 和 SQL Server:使用递归式 WITH 子句来生成一系列行,其中包含不断递增的值。实际上,在大多数 RDBMS 中,可以使用递归CTE 来解决这个问题。
sql
with x (id)
as (
select 1
union all
select id+1
from x
where id+1 <= 10
)
select * from x;
Oracle:在 Oracle Database 中,可以使用 MODEL 子句来生成行。
sql
select array id
from dual
model
dimension by (0 idx)
measures(1 array)
rules iterate (10) (
array[iteration_number] = iteration_number+1
);
PostgreSQL:使用为生成行而专门设计的函数 GENERATE_SERIES。
sql
select id
from generate_series (1, 10) x(id);
id
----
1
2
3
4
5
6
7
8
9
10
(10 rows)
MySQL:
sql
SELECT ROW_NUMBER() OVER () AS num
FROM information_schema.columns
LIMIT 10;
WITH RECURSIVE seq AS (
SELECT 1 AS num
UNION ALL
SELECT num + 1 FROM seq WHERE num < 10
)
SELECT num FROM seq;
