SQL 动态区间聚合运算

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

MySQLPostgreSQLSQL Server
MySQLPostgreSQL 和 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

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程