SQL 计算同一组或分区的行之间的差,你想返回每个员工的 DEPTNO
、ENAME
和 SAL
,以及同一个部门(即 DEPTNO
相同)里不同员工之间的工资差距。工资差距指的是当前员工的 SAL
和入职日期紧随其后的那个员工的 SAL
之间的差值(其实你希望以部门为单位考察资历和工资是否存在相关性)。对于一个部门里入职日期最晚的那个员工,将其工资差距设置为 N/A
。
SQL 计算同一组或分区的行之间的差 问题描述
你想返回每个员工的 DEPTNO
、ENAME
和 SAL
,以及同一个部门(即 DEPTNO
相同)里不同员工之间的工资差距。工资差距指的是当前员工的 SAL
和入职日期紧随其后的那个员工的 SAL
之间的差值(其实你希望以部门为单位考察资历和工资是否存在相关性)。对于一个部门里入职日期最晚的那个员工,将其工资差距设置为 N/A
。最终结果集应该如下所示。
SQL 计算同一组或分区的行之间的差 解决方案
这是说明 Oracle 窗口函数 LEAD OVER
和 LAG OVER
的便利性的另一个例子。不需要做额外的连接查询,我们就能方便地查看下一行或者前一行数据。对于其他关系数据库管理系统,可以使用标量子查询,尽管不是那么便利。对于本问题而言,当被迫要用标量子查询或自连接来解决问题时,解决方案就没有那么简单。
DB2、MySQL、PostgreSQL 和 SQL Server
用标量子查询取出紧随当前员工之后入职的员工的 HIREDATE
,然后再用另一个标量子查询找出该员工的工资。
Oracle
使用窗口函数 LEAD OVER
读取与当前行相关的下一个员工的工资。
SQL 计算同一组或分区的行之间的差 扩展知识
DB2、MySQL、PostgreSQL 和SQL Server
首先使用标量子查询找出同一个部门里紧随当前员工之后入职的员工的 HIREDATE
,本解决方案在标量子查询中使用了 MIN(HIREDATE)
来确保仅返回一个值,即使同一天入职的员工不止一个人,也只会返回一个值。
然后,使用另一个标量子查询来找出入职日期等于 NEXT_HIRE
的员工的工资。同样,本解决方案使用 MIN
函数来确保只返回一个值。
最后,计算出 SAL
和 NEXT_SAL
之间的差,并且使用 COALESCE
函数在适当的时候返回 N/A
。因为减法运算的结果既有可能是数字,也有可能是 Null
,所以必须将其转换为字符串,以便 COALESCE
函数可以正常运行。
 本解决方案使用了 `MIN(SAL)` 函数,这说明我们在某些情况下可能会不知不觉间把一些额外的业务逻辑引入查询,而我们却认为这只是一个纯粹的技术决策。如果一个给定的日期对应多个可能的工资值,我们应该选择最小值、最大值还是平均值呢?本例选择了最小值。在实际工作中,我可能更愿意将选择权交给提出报表请求的客户。
Oracle
首先使用窗口函数 LEAD OVER
为每个员工找出同部门中的“下一个”工资值。对于每个部门里最迟入职的员工,其 NEXT_SAL
列会是 Null
。
然后,计算出同一个部门里每个员工与紧随其后入职的员工的工资差值。
接下来,调用 NVL
函数,在 DIFF
等于 Null
时返回 N/A
。为了做到这一点,必须先把 DIFF
值转换为字符串,否则 NVL
函数会执行失败。
最后,调用函数 LPAD
对 DIFF
值进行格式化。这是因为在默认情况下,数字是右对齐的,而字符串则是左对齐的。调用 LPAD
能让所有结果值都变成右对齐。
本书的大部分实例都没有讨论“特例”(这是考虑到代码的可读性,也有利于我在写作过程中保持思路清晰),但是本例有必要讨论一下在使用 Oracle 的 LEAD OVER
函数时需要注意的重复项问题。对于 EMP
表里那些简单的示例数据而言,并不存在 HIREDATE
相同的行,这也是非常可能发生的状况。因而,正常情况下,我不会讨论如数据重复之类的特例(因为 EMP
表里并没有重复项)。但是,一旦涉及 LEAD
函数,有些读者很可能无法马上想到这一层(对于那些没有 Oracle 经验的读者尤其如此)。考虑如下所示的查询,该查询返回部门编号为 10 的员工之间的工资差距(按照入职先后排序计算出前后两人的工资差值)。
上述解决方案对于 EMP
表的现有数据而言毫无问题,但如果考虑重复行,就不对了。考虑如下所示的例子,有另外 4 人和员工 KING 同一天入职。
我们看到,除了 JON 以外,其他在同一天(11 月 17 日)入职的员工竟然都在和另一个同时入职的人做工资比较!这是不正确的。所有在 11 月 17 日入职的员工都应该和 MILLER 做比较。以员工 ANT 为例,ANT 的 DIFF
值是 -500,这是因为其 SAL
值相较于 JOE 少 500。事实上,ANT 的 DIFF
值应该是 -300 才对,因为其 SAL
值比 MILLER 的少 300,依据 HIREDATE
的顺序,MILLER 才是紧随 ANT 之后入职的员工。上述查询结果的错误应该归因于 LEAD OVER
函数的默认行为方式。在默认情况下,LEAD OVER
函数只往前看一行。因此,对于员工 ANT 而言,基于 HIREDATE
的下一个 SAL
值是 JOE 的 SAL
,因为 LEAD OVER
函数只会看下一行,并不会跳过重复项。幸运的是,Oracle 考虑到了这种情况,并允许我们通过传递一个额外的参数告诉 LEAD OVER
函数应该往前看几行。对于本例而言,只需要做一个计数:找出每一个在 11 月 17 日入职的员工和 1 月 23 日(MILLER 的 HIREDATE
)之间的距离。下面的解决方案展示了如何实现这一点。
现在的解决方案是正确的了。我们看到,所有在 11 月 17 日入职的员工都改为和 MILLER 做比较了。从查询结果里可以看到,现在员工 ANT 的 DIFF
值是 -300,这正是我们希望的结果。你可能不理解传递给 LEAD OVER
的表达式;其实,CNT-RN+1
代表每一个在 11 月 17 日入职的员工到 MILLER 的距离。如下所示的内嵌视图展示了 CNT
和 RN
的值。
对于每一个员工而言,CNT
值表示有多少个相同的 HIREDATE
,RN
值代表的是部门编号为 10 的员工的排名。排名基于 DEPTNO
和 HIREDATE
分组,因此只有在 HIREDATE
重复的时候,员工的排名才可能大于 1。排名基于 SAL
值排序(并不是必须这么做;只是基于 SAL 值排序比较方便,当然也可以选 EMPNO
)。现在我们知道了有多少个重复项,以及每一个重复项对应的排名,它们到 MILLER 的距离就是重复项的数目减去当前排名,然后再加 1(CNT-RN+1
)。距离计算的结果以及它对 LEAD OVER
的效果显示如下。
现在我们清楚地看到了当传递正确的距离值给 LEAD OVER
函数时会得到什么样的结果。INCORRECT
列代表了传递默认距离 1 给 LEAD OVER
函数时得到的返回值。CORRECT
列代表了为每个重复的 HIREDATE
对应的员工传递到 MILLER 的实际值给 LEAD OVER
函数时得到的返回值。至此,剩下要做的就是为每一行找出 CORRECT
和 SAL
之间的差值,这在前面已经介绍过了。