SQL 行值轮转,你想返回每个员工的姓名、工资,以及下一个最高和最低的工资值。如果没有找到更高或更低的工资值,你希望结果集可以“折回”(第一个 SAL 的前一行是最后一个 SAL;反之,最后一个 SAL 的下一行即是第一个 SAL)。
SQL 行值轮转 问题描述
你想返回每个员工的姓名、工资,以及下一个最高和最低的工资值。如果没有找到更高或更低的工资值,你希望结果集可以“折回”(第一个 SAL 的前一行是最后一个 SAL;反之,最后一个 SAL 的下一行即是第一个 SAL)。你希望返回如下所示的结果集。
ENAME SAL FORWARD REWIND ---------- ---------- ---------- ---------- SMITH 800 950 5000 JAMES 950 1100 800 ADAMS 1100 1250 950 WARD 1250 1250 1100 MARTIN 1250 1300 1250 MILLER 1300 1500 1250 TURNER 1500 1600 1300 ALLEN 1600 2450 1500 CLARK 2450 2850 1600 BLAKE 2850 2975 2450 JONES 2975 3000 2850 SCOTT 3000 3000 2975 FORD 3000 5000 3000 KING 5000 800 3000
SQL 行值轮转 解决方案
对于 Oracle 用户而言,窗口函数 LEAD OVER 和 LAG OVER 使得本问题解决起来相对容易,而且代码可读性更好。对于其他数据库,可以使用标量子查询,不过 Tie 可能会带来问题。由于存在 Tie 的问题,对于不支持窗口函数的关系数据库管理系统,我们只能提供一个近似的解决方案。
DB2、SQL Server、MySQL 和 PostgreSQL
使用标量子查询为每一个工资值找到它的下一个和前一个的工资值。
1 select e.ename, e.sal, 2 coalesce( 3 (select min(sal) from emp d where d.sal > e.sal), 4 (select min(sal) from emp) 5 ) as forward, 6 coalesce( 7 (select max(sal) from emp d where d.sal < e.sal), 8 (select max(sal) from emp) 9 ) as rewind 10 from emp e 11 order by 2
Oracle
使用窗口函数 LAG OVER 和 LEAD OVER 访问当前行的上一行和下一行记录。
1 select ename,sal, 2 nvl(lead(sal)over(order by sal),min(sal)over()) forward, 3 nvl(lag(sal)over(order by sal),max(sal)over()) rewind 4 from emp
SQL 行值轮转 扩展知识
DB2、SQL Server、MySQL 和 PostgreSQL
标量子查询方案并没有真正解决本问题。它只是一个近似的方案,当两行记录包含相同的 SAL 时,该解决方案就会返回不正确的结果。不过,在没有窗口函数可用的情况下,它已经是最好的方案了。
Oracle
(默认情况下,除非有特别指定。)窗口函数 LAG OVER 和 LEAD OVER 将分别返回当前行的上一行和下一行记录。“上一行”或“下一行”取决于 OVER 子句里的 ORDER BY 部分。如果仔细阅读本解决方案的代码,我们会发现它首先按照 SAL 排序数据集,并提取出了当前行的上一行和下一行。
select ename,sal, lead(sal)over(order by sal) forward, lag(sal)over(order by sal) rewind from emp ENAME SAL FORWARD REWIND ---------- ---------- ---------- ---------- SMITH 800 950 JAMES 950 1100 800 ADAMS 1100 1250 950 WARD 1250 1250 1100 MARTIN 1250 1300 1250 MILLER 1300 1500 1250 TURNER 1500 1600 1300 ALLEN 1600 2450 1500 CLARK 2450 2850 1600 BLAKE 2850 2975 2450 JONES 2975 3000 2850 SCOTT 3000 3000 2975 FORD 3000 5000 3000 KING 5000 3000
注意,员工 SMITH 的 REWIND 是 Null,而 KING 的 FORWARD 也是 Null;这是因为两个人的 SAL 分别是最低值和最高值。“问题”部分提到,FORWARD 或 REWIND 若出现 Null 值,则应该“折回”。这就意味着,对于最大的 SAL,FORWARD 值应为 EMP 表中最小的 SAL;而对于最小的 SAL,REWIND 值应为最大的 SAL。没有指定分区(即 OVER 子句后面跟一对空括号)的窗口函数 MIN OVER 和 MAX OVER 将分别返回最大和最小的 SAL。结果集如下所示。
select ename,sal, nvl(lead(sal)over(order by sal),min(sal)over()) forward, nvl(lag(sal)over(order by sal),max(sal)over()) rewind from emp ENAME SAL FORWARD REWIND ---------- ---------- ---------- ---------- SMITH 800 950 5000 JAMES 950 1100 800 ADAMS 1100 1250 950 WARD 1250 1250 1100 MARTIN 1250 1300 1250 MILLER 1300 1500 1250 TURNER 1500 1600 1300 ALLEN 1600 2450 1500 CLARK 2450 2850 1600 BLAKE 2850 2975 2450 JONES 2975 3000 2850 SCOTT 3000 3000 2975 FORD 3000 5000 3000 KING 5000 800 3000
LAG OVER 和 LEAD OVER 还有一个非常有用的功能,就是可以指定向前或者向后移动多少行。对于本例而言,我们只往前或往后移动了一行。如果你想往前移动 3 行,并且往后移动 5 行,做法非常简单。只需要指定移动值分别为 3 和 5 即可,如下所示。
select ename,sal, lead(sal,3)over(order by sal) forward, lag(sal,5)over(order by sal) rewind from emp ENAME SAL FORWARD REWIND ---------- ---------- ---------- ---------- SMITH 800 1250 JAMES 950 1250 ADAMS 1100 1300 WARD 1250 1500 MARTIN 1250 1600 MILLER 1300 2450 800 TURNER 1500 2850 950 ALLEN 1600 2975 1100 CLARK 2450 3000 1250 BLAKE 2850 3000 1250 JONES 2975 5000 1300 SCOTT 3000 1500 FORD 3000 1600 KING 5000 2450