SQL 动态区间聚合运算,你想执行动态的聚合运算,例如计算 EMP
表工资的动态合计。你希望把入职最早的员工的 HIREDATE
作为起始点,每隔 90 天计算一次工资合计值。你想调查一下在最早入职的员工和最近入职的员工之间每隔 90 天工资的波动状况。
SQL 动态区间聚合运算 问题描述
你想执行动态的聚合运算,例如计算 EMP
表工资的动态合计。你希望把入职最早的员工的 HIREDATE
作为起始点,每隔 90 天计算一次工资合计值。你想调查一下在最早入职的员工和最近入职的员工之间每隔 90 天工资的波动状况。你希望得到如下所示的结果集。
SQL 动态区间聚合运算 解决方案
部分数据库支持在窗口函数的帧或窗口子句中指定动态窗口,这样一来本问题的解决就变得容易多了。关键之处在于调用窗口函数时要按照 HIREDATE
排序,并指定一个为期 90 天的日期窗口,该日期窗口的起始点是第一个员工的入职日期。工资合计值的计算针对的是一组动态变化的员工,包括当前员工,以及入职时间早于当前员工但相差不超过 90 天的所有人。对于不支持这一类窗口函数的数据库,可以使用标量子查询,做法自然稍显繁琐。
DB2 和 Oracle
对于 DB2 和 Oracle,使用窗口函数 SUM OVER
,并按照 HIREDATE
排序。在窗口或“帧”子句里指定时间范围为 90 天,这样就能针对每个员工以及入职时间比他早 90 天以内的所有人的工资执行合计计算。因为 DB2 的窗口函数不支持在 ORDER BY
子句中指定 HIREDATE
(下面第 3 行代码),我们只好改用 ORDERY BY DAYS(HIREDATE)
。
相较于 DB2 解决方案,Oracle 解决方案更加简洁明了,因为 Oracle 的窗口函数支持 DATE
类型排序。
MySQL、PostgreSQL 和 SQL Server
MySQL、PostgreSQL 和 SQL Server 使用标量子查询计算工资合计值,合计值的计算范围包括当前员工,以及入职时间早于当前员工但相差不超过 90 天的所有人。
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
列的计算方法,不妨仔细观察下面的查询语句及其结果集。
仔细看 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 日对应的结果本应有两行,但这里把重复行去掉了)。
MySQL、PostgreSQL 和 SQL Server
基于 HIREDATE
排序,并使用聚合函数 SUM
以计算出每 90 天的工资合计值。除此之外,本解决方案的关键之处还包括标量子查询的运用(自连接查询也能达到同样目的)。为了方便你理解,不妨把本解决方案先转换成自连接查询,并仔细观察哪些行会被包含进求和计算。考虑如下所示的结果集,它的查询结果和前面给出的解决方案相同。
如果上述输出结果依然不够清楚明白,不妨删除分组和聚合运算部分,先打印出笛卡儿积的结果。首先使用 EMP
表产生一个笛卡儿积以便每个 HIREDATE
都可以和其他 HIREDATE
进行比较。(下面只打印出了部分结果集,因为 EMP
表的笛卡儿积会返回 196 行(14×14)数据。)
仔细查看上述结果集可以发现,除了其自身,不存在比 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 天以内的日期。
现在我们能清楚地看到哪些 SAL
值会被计入合计值了,接下来只要调用聚合函数 SUM
生成最终结果集即可。
比较上述查询和下面的查询(本例给出的标量子查询解决方案)的结果集,我们会发现它们其实是相同的。