SQL 计算当前记录和下一条记录之间的日期差

SQL 计算当前记录和下一条记录之间的日期差,计算两个日期之间相差多少天(特别是当两者分别存储于不同的行的时候)。例如,对于 DEPTNO 等于 10 的部门的每一个员工,你希望计算出他们的入职日期之间相差多少天。

SQL 计算当前记录和下一条记录之间的日期差 问题描述

计算两个日期之间相差多少天(特别是当两者分别存储于不同的行的时候)。例如,对于 DEPTNO 等于 10 的部门的每一个员工,你希望计算出他们的入职日期之间相差多少天。

SQL 计算当前记录和下一条记录之间的日期差 解决方案

要解决本问题有一个诀窍是,在早于当前入职时间的所有记录里找出 HIREDATE 的最小值。如此一来,剩下的工作就是利用 8.2 节里提到的技巧来计算出两个日期之间相差多少天。
DB2
使用标量子查询找出相对于当前 HIREDATE 的下一个 HIREDATE。然后,调用 DAYS 函数计算两个日期之间相差多少天。

1 select x.*,
2        days(x.next_hd) - days(x.hiredate) diff
3   from (
4 select e.deptno, e.ename, e.hiredate,
5        (select min(d.hiredate) from emp d
6          where d.hiredate > e.hiredate) next_hd
7   from emp e
8  where e.deptno = 10
9        ) x

MySQLSQL Server
使用标量子查询找出相对于当前 HIREDATE 的下一个 HIREDATE。然后,调用 DATEDIFF 函数计算两个日期之间相差多少天。下面的代码里以 SQL Server 版本的 DATEDIFF 函数为例。

1 select x.*,
2        datediff(day,x.hiredate,x.next_hd) diff
3   from (
4 select e.deptno, e.ename, e.hiredate,
5        (select min(d.hiredate) from emp d
6          where d.hiredate > e.hiredate) next_hd
7   from emp e
8  where e.deptno = 10
9        ) x

对于 MySQL 版本的 DATEDIFF 函数,我们需要省略第一个参数 day,并把剩下的两个参数的顺序颠倒过来。

2        datediff(x.next_hd, x.hiredate) diff

Oracle
对于 Oracle 8i 及后续版本,使用窗口函数 LEAD OVER 访问相对于当前 HIREDATE 的下一个 HIREDATE,然后执行减法运算。

1  select ename, hiredate, next_hd,
2         next_hd - hiredate diff
3    from (
4  select deptno, ename, hiredate,
5         lead(hiredate)over(order by hiredate) next_hd
6    from emp
7         )
8   where deptno=10

对于 Oracle 8i 及更早的版本,则需要采用下面的 PostgreSQL 解决方案。
PostgreSQL
使用标量子查询找出相对于当前 HIREDATE 的下一个 HIREDATE。然后,直接利用减法运算得出两者相差多少天。

1 select x.*,
2        x.next_hd - x.hiredate as diff
3   from (
4 select e.deptno, e.ename, e.hiredate,
5        (select min(d.hiredate) from emp d
6          where d.hiredate > e.hiredate) as next_hd
7   from emp e
8  where e.deptno = 10
9        ) x

SQL 计算当前记录和下一条记录之间的日期差 扩展知识

DB2、MySQL、PostgreSQL 和 SQL Server
除了语法上的差别,所有这些解决方案的做法是相同的:使用标量子查询找出相对于当前 HIREDATE 的下一个 HIREDATE,然后使用本章 8.2 节里用过的技巧计算出两个日期之间相差多少天。
Oracle
窗口函数 LEAD OVER 非常有用,它可以访问“未来”的行(“未来”是相对于当前行而言的,由 ORDER BY 子句决定)。不必使用额外的连接查询就可以访问当前行前后的行数据,这种能力有助于我们写出更高效、可读性更好的代码。当使用窗口函数时,要记住它们是在 WHERE 子句之后才被评估执行的,这就是为什么本解决方案需要一个内嵌视图。如果我们把过滤 DEPTNO 的动作移到内嵌视图里面的话,结果就不一样了(就变成了只考虑 DEPTNO 等于 10 的员工的 HIREDATE)。关于 Oracle 的 LEADLAG 函数,需要特别指出的一点是它们对于重复项的处理。在前言部分我提到过,本书的实例都不包含“防御性代码”,因为有太多无法预见的状况都可能导致代码无法正常执行。也就是说,即使我们能预见到每一种可能出现的问题,但最终写出的 SQL 却可能已经冗繁到不具有可读性。因此,在大多数情况下,一个解决方案的意义在于它提供了一种技巧:我们能将其用于线上系统,但是又必须事先做好测试,并针对具体的数据做出必要的调整。对于本例而言,有一种情况稍后需要简单讨论一下,因为针对它的变通方案不是那么显而易见,对于不熟悉 Oracle 的读者而言尤其如此。本例中 EMP 表里不存在重复的 HIREDATE,但是在一个表里出现重复的日期当然是可能的(并且非常可能)。考虑 DEPTNO 等于 10 的员工及其对应的 HIREDATE

select ename, hiredate
  from emp
 where deptno=10
 order by 2
 
ENAME  HIREDATE
------ -----------
CLARK  09-JUN-1981
KING   17-NOV-1981
MILLER 23-JAN-1982

为了讨论的需要,我们插入 4 条重复数据,这样就有(包括 KING 在内的)5 个员工的 HIREDATE 都是 11 月 17 日了。

insert into emp (empno,ename,deptno,hiredate)
values (1,'ant',10,to_date('17-NOV-1981'))
 
insert into emp (empno,ename,deptno,hiredate)
values (2,'joe',10,to_date('17-NOV-1981'))
 
insert into emp (empno,ename,deptno,hiredate)
values (3,'jim',10,to_date('17-NOV-1981'))
 
insert into emp (empno,ename,deptno,hiredate)
values (4,'choi',10,to_date('17-NOV-1981'))
 
select ename, hiredate
  from emp
 where deptno=10
 order by 2
 
ENAME  HIREDATE
------ -----------
CLARK  09-JUN-1981
ant    17-NOV-1981
joe    17-NOV-1981
KING   17-NOV-1981
jim    17-NOV-1981
choi   17-NOV-1981
MILLER 23-JAN-1982

现在 DEPTNO 等于 10 的部门里就有不止一个人的 HIREDATE 是同一天了。如果仍然使用上述解决方案(但是要把 DEPTNO 过滤动作移到内嵌视图里面去,我们只关注 DEPTNO 等于 10 的员工及其 HIREDATE),返回的结果集就变成如下所示的输出内容。

select ename, hiredate, next_hd,
       next_hd - hiredate diff
  from (
select deptno, ename, hiredate,
       lead(hiredate)over(order by hiredate) next_hd
  from emp
 where deptno=10
        )
 
ENAME  HIREDATE    NEXT_HD           DIFF
------ ----------- ----------- ----------
CLARK  09-JUN-1981 17-NOV-1981        161
ant    17-NOV-1981 17-NOV-1981          0
joe    17-NOV-1981 17-NOV-1981          0
KING   17-NOV-1981 17-NOV-1981          0
jim    17-NOV-1981 17-NOV-1981          0
choi   17-NOV-1981 23-JAN-1982         67
MILLER 23-JAN-1982 (null)          (null)

看一下 HIREDATE 相同的 5 个员工,他们中有 4 个人的 DIFF 值是 0。这显然不正确。对于 HIREDATE 相同的员工而言,应该与下一个不同的 HIREDATE 相比较,例如 HIREDATE 是 11 月 17 日的员工要与 MILLER 的 HIREDATE 相比较。问题的根源在于 LEAD 函数仅仅按照 HIREDATE 排序,却不会自动去重。因此,如果把 ANT 的 HIREDATE 与 JOE 的相比较,相差的天数就是 0,如此一来 ANT 的 DIFF 值就变成 0 了。所幸 Oracle 已经为这种情况提供了一个简单的变通方案。当调用 LEAD 函数的时候,我们可以传递一个参数用于指定“未来行”的位置(例如,它位于下一行,或者隔了 10 行之后,等等)。因此,对于员工 ANT,需要跨过 5 行,而不是一行(我们希望跳过所有重复的 HIREDATE),去看一下 MILLER 的 HIREDATE。员工 JOE 距离 MILLER 有 4 行的距离,JIM 则是 3 行,KING 是 2 行,CHOI 幸运地只相隔一行。为了获取正确答案,只要把上述每一个员工所在行到 MILLER 的距离值作为参数传递给 LEAD 函数即可。该解决方案如下所示。

select ename, hiredate, next_hd,
       next_hd - hiredate diff
  from (
select deptno, ename, hiredate,
       lead(hiredate,cnt-rn+1)over(order by hiredate) next_hd
  from (
select deptno,ename,hiredate,
       count(*)over(partition by hiredate) cnt,
       row_number()over(partition by hiredate order by empno) rn
  from emp
 where deptno=10
       )
       )
 
ENAME  HIREDATE    NEXT_HD           DIFF
------ ----------- ----------- ----------
CLARK  09-JUN-1981 17-NOV-1981        161
ant    17-NOV-1981 23-JAN-1982         67
joe    17-NOV-1981 23-JAN-1982         67
jim    17-NOV-1981 23-JAN-1982         67
choi   17-NOV-1981 23-JAN-1982         67
KING   17-NOV-1981 23-JAN-1982         67
MILLER 23-JAN-1982 (null)          (null)

现在我们能得到正确的计算结果了。所有 HIREDATE 相同的员工都会和下一个不同的 HIREDATE 相比较,而不会匹配到一个相同的 HIREDATE 值。如果这个替代方案不是那么容易理解,不妨把上述查询分解开来看。先从内嵌视图开始。

select deptno,ename,hiredate,
       count(*)over(partition by hiredate) cnt,
       row_number()over(partition by hiredate order by empno) rn
  from emp
 where deptno=10
 
DEPTNO ENAME  HIREDATE           CNT         RN
------ ------ ----------- ---------- ----------
    10 CLARK  09-JUN-1981          1          1
    10 ant    17-NOV-1981          5          1
    10 joe    17-NOV-1981          5          2
    10 jim    17-NOV-1981          5          3
    10 choi   17-NOV-1981          5          4
    10 KING   17-NOV-1981          5          5
    10 MILLER 23-JAN-1982          1          1

窗口函数 COUNT OVER 计算每一种 HIREDATE 值出现的次数,并为每一行记录返回该值。对于那个重复的 HIREDATECNT 的值都是 5。窗口函数 ROW_NUMBER OVER 按照 EMPNO 为每一个员工排名。排名按照 HIREDATE 分区,除非有重复的 HIREDATE 出现,否则每个员工对应的 RN 值都是 1。现在,所有重复数据都被分组计数并算出其在分组里的排名,而该排名值可以用于度量当前 HIREDATE 到下一个 HIREDATE(MILLER 的 HIREDATE)的距离。调用 LEAD 函数时,我们通过从 CNT 里减去 RN 值并加 1 来得出该距离值。

select deptno, ename, hiredate,
       cnt-rn+1 distance_to_miller,
       lead(hiredate,cnt-rn+1)over(order by hiredate) next_hd
  from (
select deptno,ename,hiredate,
       count(*)over(partition by hiredate) cnt,
       row_number()over(partition by hiredate order by empno) rn
  from emp
 where deptno=10
        )
 
DEPTNO ENAME  HIREDATE    DISTANCE_TO_MILLER NEXT_HD
------ ------ ----------- ------------------ -----------
    10 CLARK  09-JUN-1981                  1 17-NOV-1981
    10 ant    17-NOV-1981                  5 23-JAN-1982
    10 joe    17-NOV-1981                  4 23-JAN-1982
    10 jim    17-NOV-1981                  3 23-JAN-1982
    10 choi   17-NOV-1981                  2 23-JAN-1982
    10 KING   17-NOV-1981                  1 23-JAN-1982
    10 MILLER 23-JAN-1982                  1 (null)

如上所示,通过传递适当的距离值以跳过若干行重复数据,LEAD 函数就能实现正确的日期比较了。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程

SQL 实例