SQL 动态区间聚合运算,你想执行动态的聚合运算,例如计算 EMP
表工资的动态合计。你希望把入职最早的员工的 HIREDATE
作为起始点,每隔 90 天计算一次工资合计值。你想调查一下在最早入职的员工和最近入职的员工之间每隔 90 天工资的波动状况。
SQL 动态区间聚合运算 问题描述
你想执行动态的聚合运算,例如计算 EMP
表工资的动态合计。你希望把入职最早的员工的 HIREDATE
作为起始点,每隔 90 天计算一次工资合计值。你想调查一下在最早入职的员工和最近入职的员工之间每隔 90 天工资的波动状况。你希望得到如下所示的结果集。
HIREDATE SAL SPENDING_PATTERN
----------- ------- ----------------
17-DEC-1980 800 800
20-FEB-1981 1600 2400
22-FEB-1981 1250 3650
02-APR-1981 2975 5825
01-MAY-1981 2850 8675
09-JUN-1981 2450 8275
08-SEP-1981 1500 1500
28-SEP-1981 1250 2750
17-NOV-1981 5000 7750
03-DEC-1981 950 11700
03-DEC-1981 3000 11700
23-JAN-1982 1300 10250
09-DEC-1982 3000 3000
12-JAN-1983 1100 4100
SQL 动态区间聚合运算 解决方案
部分数据库支持在窗口函数的帧或窗口子句中指定动态窗口,这样一来本问题的解决就变得容易多了。关键之处在于调用窗口函数时要按照 HIREDATE
排序,并指定一个为期 90 天的日期窗口,该日期窗口的起始点是第一个员工的入职日期。工资合计值的计算针对的是一组动态变化的员工,包括当前员工,以及入职时间早于当前员工但相差不超过 90 天的所有人。对于不支持这一类窗口函数的数据库,可以使用标量子查询,做法自然稍显繁琐。
DB2 和 Oracle
对于 DB2 和 Oracle,使用窗口函数 SUM OVER
,并按照 HIREDATE
排序。在窗口或“帧”子句里指定时间范围为 90 天,这样就能针对每个员工以及入职时间比他早 90 天以内的所有人的工资执行合计计算。因为 DB2 的窗口函数不支持在 ORDER BY
子句中指定 HIREDATE
(下面第 3 行代码),我们只好改用 ORDERY BY DAYS(HIREDATE)
。
1 select hiredat,
2 sal,
3 sum(sal)over(order by days(hiredate)
4 range between 90 preceding
5 and current row) spending_pattern
6 from emp e
相较于 DB2 解决方案,Oracle 解决方案更加简洁明了,因为 Oracle 的窗口函数支持 DATE
类型排序。
1 select hiredate,
2 sal,
3 sum(sal)over(order by hiredate
4 range between 90 preceding
5 and current row) spending_pattern
6 from emp e
MySQL、PostgreSQL 和 SQL Server
MySQL、PostgreSQL 和 SQL Server 使用标量子查询计算工资合计值,合计值的计算范围包括当前员工,以及入职时间早于当前员工但相差不超过 90 天的所有人。
1 select e.hiredate,
2 e.sal,
3 (select sum(sal) from emp d
4 where d.hiredate between e.hiredate-90
5 and e.hiredate) as spending_pattern
6 from emp e
7 order by 1
SQL 动态区间聚合运算 扩展知识
DB2 和 Oracle
DB2 和 Oracle 的解决方案大致相同。两者差别极小,唯一的不同之处在于窗口函数的 ORDER BY
子句如何操作 HIREDATE
。在写作本书时,如果想通过一个数值来指定窗口的范围,DB2 是不支持在 ORDER BY
子句中使用 DATE
类型的。(例如,RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
支持日期类型的排序,但 RANGE BETWEEN 90 PRECEDING AND CURRENT ROW
却不支持这么做。)
掌握本解决方案的关键是要了解查询语句中窗口子句的工作原理。首先,我们定义好的窗口会按照 HIREDATE
对所有员工的工资进行排序。然后,调用聚合函数计算工资总额。但是,这里计算出的并不是所有工资的总和。具体的处理过程如下所示。
(1) 评估最早入职的员工的工资。第一个员工入职之前不会有其他人已经入职,因而此时的工资总额就是第一个员工的工资。
(2) (按照 HIREDATE
的先后顺序)评估下一位员工的工资。该员工的工资会被计入动态工资合计值,其他入职时间比他早、但相差 90 天以内的员工的工资也会被计入合计值。
第一个员工的 HIREDATE
是 1980 年 12 月 17 日,紧接着入职的下一位员工的 HIREDATE
是 1981 年 2 月 20 日。第二个员工的入职日期和第一个员工相差不足 90 天,因此第二个员工对应的动态工资合计值等于 2400(1600 + 800)。为了更深入地理解 SPENDING_PATTERN
列的计算方法,不妨仔细观察下面的查询语句及其结果集。
select distinct
dense_rank()over(order by e.hiredate) window,
e.hiredate current_hiredate,
d.hiredate hiredate_within_90_days,
d.sal sals_used_for_sum
from emp e,
emp d
where d.hiredate between e.hiredate-90 and e.hiredate
WINDOW CURRENT_HIREDATE HIREDATE_WITHIN_90_DAYS SALS_USED_FOR_SUM
------ ---------------- ----------------------- -----------------
1 17-DEC-1980 17-DEC-1980 800
2 20-FEB-1981 17-DEC-1980 800
2 20-FEB-1981 20-FEB-1981 1600
3 22-FEB-1981 17-DEC-1980 800
3 22-FEB-1981 20-FEB-1981 1600
3 22-FEB-1981 22-FEB-1981 1250
4 02-APR-1981 20-FEB-1981 1600
4 02-APR-1981 22-FEB-1981 1250
4 02-APR-1981 02-APR-1981 2975
5 01-MAY-1981 20-FEB-1981 1600
5 01-MAY-1981 22-FEB-1981 1250
5 01-MAY-1981 02-APR-1981 2975
5 01-MAY-1981 01-MAY-1981 2850
6 09-JUN-1981 02-APR-1981 2975
6 09-JUN-1981 01-MAY-1981 2850
6 09-JUN-1981 09-JUN-1981 2450
7 08-SEP-1981 08-SEP-1981 1500
8 28-SEP-1981 08-SEP-1981 1500
8 28-SEP-1981 28-SEP-1981 1250
9 17-NOV-1981 08-SEP-1981 1500
9 17-NOV-1981 28-SEP-1981 1250
9 17-NOV-1981 17-NOV-1981 5000
10 03-DEC-1981 08-SEP-1981 1500
10 03-DEC-1981 28-SEP-1981 1250
10 03-DEC-1981 17-NOV-1981 5000
10 03-DEC-1981 03-DEC-1981 950
10 03-DEC-1981 03-DEC-1981 3000
11 23-JAN-1982 17-NOV-1981 5000
11 23-JAN-1982 03-DEC-1981 950
11 23-JAN-1982 03-DEC-1981 3000
11 23-JAN-1982 23-JAN-1982 1300
12 09-DEC-1982 09-DEC-1982 3000
13 12-JAN-1983 09-DEC-1982 3000
13 12-JAN-1983 12-JAN-1983 1100
仔细看 WINDOW
列的话,我们会发现具有相同 WINDOW
值的行会被计入动态工资合计值。以 WINDOW
值等于 3 的那一组为例,该窗口中被计入合计的工资分别是 800、1600 和 1250,合计为 3650。回头再看一下“问题”部分给出的最终结果集,我们看到 1981 年 2 月 22 日 (WINDOW 3)
那一行的 SPENDING_PATTERN
值确实是 3650。如果想证明自连接查询为每个窗口都找到了正确的工资,我们只要按照 CURRENT_DATE
分组,并把 SALS_USED_FOR_SUM
值相加求和即可。查询结果应该会和“问题”部分给出的结果集相一致(1981 年 12 月 3 日对应的结果本应有两行,但这里把重复行去掉了)。
select current_hiredate,
sum(sals_used_for_sum) spending_pattern
from (
select distinct
dense_rank()over(order by e.hiredate) window,
e.hiredate current_hiredate,
d.hiredate hiredate_within_90_days,
d.sal sals_used_for_sum
from emp e,
emp d
where d.hiredate between e.hiredate-90 and e.hiredate
) x
group by current_hiredate
CURRENT_HIREDATE SPENDING_PATTERN
---------------- ----------------
17-DEC-1980 800
20-FEB-1981 2400
22-FEB-1981 3650
02-APR-1981 5825
01-MAY-1981 8675
09-JUN-1981 8275
08-SEP-1981 1500
28-SEP-1981 2750
17-NOV-1981 7750
03-DEC-1981 11700
23-JAN-1982 10250
09-DEC-1982 3000
12-JAN-1983 4100
MySQL、PostgreSQL 和 SQL Server
基于 HIREDATE
排序,并使用聚合函数 SUM
以计算出每 90 天的工资合计值。除此之外,本解决方案的关键之处还包括标量子查询的运用(自连接查询也能达到同样目的)。为了方便你理解,不妨把本解决方案先转换成自连接查询,并仔细观察哪些行会被包含进求和计算。考虑如下所示的结果集,它的查询结果和前面给出的解决方案相同。
select e.hiredate,
e.sal,
sum(d.sal) as spending_pattern
from emp e, emp d
where d.hiredate
between e.hiredate-90 and e.hiredate
group by e.hiredate,e.sal
order by 1
HIREDATE SAL SPENDING_PATTERN
----------- ----- ----------------
17-DEC-1980 800 800
20-FEB-1981 1600 2400
22-FEB-1981 1250 3650
02-APR-1981 2975 5825
01-MAY-1981 2850 8675
09-JUN-1981 2450 8275
08-SEP-1981 1500 1500
28-SEP-1981 1250 2750
17-NOV-1981 5000 7750
03-DEC-1981 950 11700
03-DEC-1981 3000 11700
23-JAN-1982 1300 10250
09-DEC-1982 3000 3000
12-JAN-1983 1100 4100
如果上述输出结果依然不够清楚明白,不妨删除分组和聚合运算部分,先打印出笛卡儿积的结果。首先使用 EMP
表产生一个笛卡儿积以便每个 HIREDATE
都可以和其他 HIREDATE
进行比较。(下面只打印出了部分结果集,因为 EMP
表的笛卡儿积会返回 196 行(14×14)数据。)
select e.hiredate,
e.sal,
d.sal,
d.hiredate
from emp e, emp d
HIREDATE SAL SAL HIREDATE
----------- ----- ----- -----------
17-DEC-1980 800 800 17-DEC-1980
17-DEC-1980 800 1600 20-FEB-1981
17-DEC-1980 800 1250 22-FEB-1981
17-DEC-1980 800 2975 02-APR-1981
17-DEC-1980 800 1250 28-SEP-1981
17-DEC-1980 800 2850 01-MAY-1981
17-DEC-1980 800 2450 09-JUN-1981
17-DEC-1980 800 3000 09-DEC-1982
17-DEC-1980 800 5000 17-NOV-1981
17-DEC-1980 800 1500 08-SEP-1981
17-DEC-1980 800 1100 12-JAN-1983
17-DEC-1980 800 950 03-DEC-1981
17-DEC-1980 800 3000 03-DEC-1981
17-DEC-1980 800 1300 23-JAN-1982
20-FEB-1981 1600 800 17-DEC-1980
20-FEB-1981 1600 1600 20-FEB-1981
20-FEB-1981 1600 1250 22-FEB-1981
20-FEB-1981 1600 2975 02-APR-1981
20-FEB-1981 1600 1250 28-SEP-1981
20-FEB-1981 1600 2850 01-MAY-1981
20-FEB-1981 1600 2450 09-JUN-1981
20-FEB-1981 1600 3000 09-DEC-1982
20-FEB-1981 1600 5000 17-NOV-1981
20-FEB-1981 1600 1500 08-SEP-1981
20-FEB-1981 1600 1100 12-JAN-1983
20-FEB-1981 1600 950 03-DEC-1981
20-FEB-1981 1600 3000 03-DEC-1981
20-FEB-1981 1600 1300 23-JAN-1982
仔细查看上述结果集可以发现,除了其自身,不存在比 1980 年 12 月 17 日更早的 HIREDATE
。因此,那一行对应的合计值应该是 800。然后是下一个 HIREDATE
,1981 年 2 月 20 日,我们注意到只有一个 HIREDATE
落在了 90 天的窗口范围内(比该日期早 90 天),那就是 1980 年 12 月 17 日。为 1980 年 12 月 17 日对应的 SAL
值和 1981 年 2 月 20 对应的 SAL
值求和,结果是 2400(因为我们寻找的是当前 HIREDATE
以及 90 天之内的日期),这恰好就是最终结果集该 HIREDATE
对应的工资合计值。
现在我们已经理解具体的做法了,接着在 WHERE
子句里添加一个条件以筛选出每一个 HIREDATE
以及早于该 HIREDATE
90 天以内的日期。
select e.hiredate,
e.sal,
d.sal sal_to_sum,
d.hiredate within_90_days
from emp e, emp d
where d.hiredate
between e.hiredate-90 and e.hiredate
order by 1
HIREDATE SAL SAL_TO_SUM WITHIN_90_DAYS
----------- ----- ---------- --------------
17-DEC-1980 800 800 17-DEC-1980
20-FEB-1981 1600 800 17-DEC-1980
20-FEB-1981 1600 1600 20-FEB-1981
22-FEB-1981 1250 800 17-DEC-1980
22-FEB-1981 1250 1600 20-FEB-1981
22-FEB-1981 1250 1250 22-FEB-1981
02-APR-1981 2975 1600 20-FEB-1981
02-APR-1981 2975 1250 22-FEB-1981
02-APR-1981 2975 2975 02-APR-1981
01-MAY-1981 2850 1600 20-FEB-1981
01-MAY-1981 2850 1250 22-FEB-1981
01-MAY-1981 2850 2975 02-APR-1981
01-MAY-1981 2850 2850 01-MAY-1981
09-JUN-1981 2450 2975 02-APR-1981
09-JUN-1981 2450 2850 01-MAY-1981
09-JUN-1981 2450 2450 09-JUN-1981
08-SEP-1981 1500 1500 08-SEP-1981
28-SEP-1981 1250 1500 08-SEP-1981
28-SEP-1981 1250 1250 28-SEP-1981
17-NOV-1981 5000 1500 08-SEP-1981
17-NOV-1981 5000 1250 28-SEP-1981
17-NOV-1981 5000 5000 17-NOV-1981
03-DEC-1981 950 1500 08-SEP-1981
03-DEC-1981 950 1250 28-SEP-1981
03-DEC-1981 950 5000 17-NOV-1981
03-DEC-1981 950 950 03-DEC-1981
03-DEC-1981 950 3000 03-DEC-1981
03-DEC-1981 3000 1500 08-SEP-1981
03-DEC-1981 3000 1250 28-SEP-1981
03-DEC-1981 3000 5000 17-NOV-1981
03-DEC-1981 3000 950 03-DEC-1981
03-DEC-1981 3000 3000 03-DEC-1981
23-JAN-1982 1300 5000 17-NOV-1981
23-JAN-1982 1300 950 03-DEC-1981
23-JAN-1982 1300 3000 03-DEC-1981
23-JAN-1982 1300 1300 23-JAN-1982
09-DEC-1982 3000 3000 09-DEC-1982
12-JAN-1983 1100 3000 09-DEC-1982
12-JAN-1983 1100 1100 12-JAN-1983
现在我们能清楚地看到哪些 SAL
值会被计入合计值了,接下来只要调用聚合函数 SUM
生成最终结果集即可。
select e.hiredate,
e.sal,
sum(d.sal) as spending_pattern
from emp e, emp d
where d.hiredate
between e.hiredate-90 and e.hiredate
group by e.hiredate,e.sal
order by 1
比较上述查询和下面的查询(本例给出的标量子查询解决方案)的结果集,我们会发现它们其实是相同的。
select e.hiredate,
e.sal,
(select sum(sal) from emp d
where d.hiredate between e.hiredate-90
and e.hiredate) as spending_pattern
from emp e
order by 1
HIREDATE SAL SPENDING_PATTERN
----------- ----- ----------------
17-DEC-1980 800 800
20-FEB-1981 1600 2400
22-FEB-1981 1250 3650
02-APR-1981 2975 5825
01-MAY-1981 2850 8675
09-JUN-1981 2450 8275
08-SEP-1981 1500 1500
28-SEP-1981 1250 2750
17-NOV-1981 5000 7750
03-DEC-1981 950 11700
03-DEC-1981 3000 11700
23-JAN-1982 1300 10250
09-DEC-1982 3000 3000
12-JAN-1983 1100 4100