SQL经典实例——日期算术运算

日期算术运算

1、加上或减去若干天、若干月或若干年

问题:你需要给某个日期加上或减去若干天、若干月或若干年。例如,你想根据员工 CLARK 的 HIREDATE,返回 6 个日期:CLARK 获聘前 5 天和后 5 天,CLARK 获聘前 5个月和后 5 个月,CLARK 获聘前 5 年和后 5 年。CLARK 获聘于 2006 年 6 月 9 日,因此你要返回如下结果集。

sql 复制代码
HD_MINUS_5D HD_PLUS_5D  HD_MINUS_5M HD_PLUS_5M  HD_MINUS_5Y HD_PLUS_5Y
----------- ----------- ----------- ----------- ----------- -----------
04-JUN-2006 14-JUN-2006 09-JAN-2006 09-NOV-2006 09-JUN-2001 09-JUN-2001
12-NOV-2006 22-NOV-2006 17-JUN-2006 17-APR-2007 17-NOV-2001 17-NOV-2001
18-JAN-2007 28-JAN-2007 23-AUG-2006 23-JUN-2007 23-JAN-2002 23-JAN-2002

解决方案

DB2:标准加法运算和减法运算可用于日期值,但在给某个日期加上或减去的值后面,必须指定其单位。

sql 复制代码
select hiredate -5 day   as hd_minus_5D,
       hiredate +5 day   as hd_plus_5D,
       hiredate -5 month as hd_minus_5M,
       hiredate +5 month as hd_plus_5M,
       hiredate -5 year  as hd_minus_5Y,
       hiredate +5 year  as hd_plus_5Y
  from emp
 where deptno = 10

Oracle:要加上或减去若干天,可以使用标准加法运算和减法运算;要加上或减去若干月或若干年,可以使用函数ADD_MONTHS。

sql 复制代码
select hiredate-5                 as hd_minus_5D,
       hiredate+5                 as hd_plus_5D,
       add_months(hiredate,-5)    as hd_minus_5M,
       add_months(hiredate,5)     as hd_plus_5M,
       add_months(hiredate,-5*12) as hd_minus_5Y,
       add_months(hiredate,5*12)  as hd_plus_5Y
  from emp
 where deptno = 10

PostgreSQL:使用标准加法运算和减法运算以及关键字 INTERVAL 指定时间单位和数量。单位和数量必须放在单引号内。

sql 复制代码
select hiredate - interval '5 day'   as hd_minus_5D,
       hiredate + interval '5 day'   as hd_plus_5D,
       hiredate - interval '5 month' as hd_minus_5M,
       hiredate + interval '5 month' as hd_plus_5M,
       hiredate - interval '5 year'  as hd_minus_5Y,
       hiredate + interval '5 year'  as hd_plus_5Y
  from emp
 where deptno=10;


     hd_minus_5d     |     hd_plus_5d      |     hd_minus_5m     |     hd_plus_5m      |     hd_minus_5y     |     hd_plus_5y      
---------------------+---------------------+---------------------+---------------------+---------------------+---------------------
 2007-01-18 00:00:00 | 2007-01-28 00:00:00 | 2006-08-23 00:00:00 | 2007-06-23 00:00:00 | 2002-01-23 00:00:00 | 2012-01-23 00:00:00
 2006-11-12 00:00:00 | 2006-11-22 00:00:00 | 2006-06-17 00:00:00 | 2007-04-17 00:00:00 | 2001-11-17 00:00:00 | 2011-11-17 00:00:00
 2006-06-04 00:00:00 | 2006-06-14 00:00:00 | 2006-01-09 00:00:00 | 2006-11-09 00:00:00 | 2001-06-09 00:00:00 | 2011-06-09 00:00:00
(3 rows)

MySQL:使用标准加法运算和减法运算以及关键字 INTERVAL 指定时间单位和数量。与 PostgreSQL 解决方案不同,单位和数量无须放在单引号内。

sql 复制代码
select hiredate - interval 5 day   as hd_minus_5D,
       hiredate + interval 5 day   as hd_plus_5D,
       hiredate - interval 5 month as hd_minus_5M,
       hiredate + interval 5 month as hd_plus_5M,
       hiredate - interval 5 year  as hd_minus_5Y,
       hiredate + interval 5 year  as hd_plus_5Y
  from emp
 where deptno=10;

也可以使用函数 DATE_ADD。

sql 复制代码
select date_add(hiredate,interval -5 day)   as hd_minus_5D,
       date_add(hiredate,interval  5 day)   as hd_plus_5D,
       date_add(hiredate,interval -5 month) as hd_minus_5M,
       date_add(hiredate,interval  5 month) as hd_plus_5M,
       date_add(hiredate,interval -5 year)  as hd_minus_5Y,
       date_add(hiredate,interval  5 year)  as hd_plus_5DY
  from emp;

SQL Server:使用函数 DATEADD 给指定日期加上或减去指定单位和数量的时间。

sql 复制代码
select dateadd(day,-5,hiredate)   as hd_minus_5d,
       dateadd(day,5,hiredate)    as hd_plus_5d,
       dateadd(month,-5,hiredate) as hd_minus_5m,
       dateadd(month,5,hiredate)  as hd_plus_5m,
       dateadd(year,-5,hiredate)  as hd_minus_5y,
       dateadd(year,5,hiredate)   as hd_plus_5y
  from emp
 where deptno = 10;

2、确定两个日期相差多少天

问题:你想找出两个日期相隔多长时间,并在表示结果时以天为单位。例如,你想找出员工 ALLEN 和 WARD 的HIREDATE 之间有多少天。

解决方案

DB2:使用两个内嵌视图找出 ALLEN 和 WARD 的HIREDATE,再使用函数 DAYS 将这两个日期相减。

sql 复制代码
 select days(ward_hd) - days(allen_hd)
   from (
 select hiredate as ward_hd
   from emp
  where ename = 'WARD'
        ) x,
        (
 select hiredate as allen_hd
   from emp
  where ename = 'ALLEN'
        ) y;

Oracle 和 PostgreSQL:使用两个内嵌视图找出 ALLEN 和 WARD 的HIREDATE,再将这两个日期相减。

sql 复制代码
select ward_hd - allen_hd
from (
	select hiredate as ward_hd
	from emp
	where ename = 'WARD'
) x, (
	select hiredate as allen_hd
	from emp
	where ename = 'ALLEN'
) y;

MySQL 和 SQL Server:使用函数 DATEDIFF 找出两个日期相差多少天。在MySQL 中,函数 DATEDIFF 接受两个参数(两个日期,你要找出它们相差多少天)​,并要求将较早的那个日期作为第一个参数(SQL Server 中与此相反)​。在 SQLServer 中,函数 DATEDIFF 允许你指定要返回的值的单位(在本实例中,你希望返回相差的天数)​。下面是SQL Server 的解决方案。

sql 复制代码
select datediff(day,allen_hd,ward_hd)
from (
	select hiredate as ward_hd
	from emp
	where ename = 'WARD'
) x, (
	select hiredate as allen_hd
	from emp
	where ename = 'ALLEN'
) y;

3、确定两个日期之间有多少个工作日

问题:给定两个日期,你想找出它们之间有多少个工作日(包括这两个日期本身)​。如果 1 月 10 日是星期一,1 月 11 日是星期二,那么这两个日期之间有两个工作日,因为这两个日期都是工作日。在本实例中,只要不是星期六或星期日,就认为是工作日。

解决方案:本解决方案旨在找出 BLAKE 和 JONES 的 HIREDATE之间有多少个工作日。为了确定两个日期之间的工作日数量,可以使用一张透视表。在这张表中,两个日期之间的每一天(包含这两个日期本身)都有对应的一行。这样,要计算工作日数,只需计算在返回的日期中,有多少个日期不是星期六或星期日。

如果要剔除节假日,那么可以创建一个HOLIDAYS 表,并在解决方案中使用谓词 NOT IN 将 HOLIDAYS 表中的日期排除在外。

DB2:使用透视表 T500 生成足够的行数,用于表示两个日期之间的每一天。再计算不是周末的天数。使用函数DAYNAME 来确定各个日期是星期几。

sql 复制代码
 select sum(case when dayname(jones_hd+t500.id day -1 day)
                   in ( 'Saturday','Sunday' )
                 then 0 else 1
            end) as days
   from (
 select max(case when ename = 'BLAKE'
                 then hiredate
            end) as blake_hd,
        max(case when ename = 'JONES'
                 then hiredate
            end) as jones_hd
   from emp
  where ename in ( 'BLAKE','JONES' )
        ) x,
        t500
  where t500.id <= blake_hd-jones_hd+1;

MySQL:使用透视表 T500 生成足够的行数,用于表示两个日期之间的每一天。再计算不是周末的天数。使用函数DATE_ADD 给起始日期加上若干天,以获得各个中间日期。使用函数 DATE_FORMAT 确定各个日期是星期几。

sql 复制代码
 select sum(case when date_format(
                         date_add(jones_hd,
                                  interval t500.id-1 DAY),'%a')
                   in ( 'Sat','Sun' )
                 then 0 else 1
            end) as days
   from (
 select max(case when ename = 'BLAKE'
                 then hiredate
            end) as blake_hd,
        max(case when ename = 'JONES'
                 then hiredate
             end) as jones_hd
   from emp
  where ename in ( 'BLAKE','JONES' )
        ) x,
        t500
  where t500.id <= datediff(blake_hd,jones_hd)+1;

Oracle:使用透视表 T500 生成足够的行数,用于表示两个日期之间的每一天。再计算不是周末的天数。使用函数TO_CHAR 确定各个日期是星期几。

sql 复制代码
 select sum(case when to_char(jones_hd+t500.id-1,'DY')
                   in ( 'SAT','SUN' )
                 then 0 else 1
            end) as days
   from (
 select max(case when ename = 'BLAKE'
                 then hiredate
            end) as blake_hd,
        max(case when ename = 'JONES'
                 then hiredate
            end) as jones_hd
   from emp
  where ename in ( 'BLAKE','JONES' )
        ) x,
        t500
  where t500.id <= blake_hd-jones_hd+1;

PostgreSQL:使用透视表 T500 生成足够的行数,用于表示两个日期之间的每一天。再计算不是周末的天数。使用函数TO_CHAR 确定各个日期是星期几。

sql 复制代码
 select sum(case when trim(to_char(jones_hd+t500.id-1,'DAY'))
                   in ( 'SATURDAY','SUNDAY' )
                 then 0 else 1
            end) as days
   from (
 select max(case when ename = 'BLAKE'
                 then hiredate
            end) as blake_hd,
        max(case when ename = 'JONES'
                 then hiredate
            end) as jones_hd
   from emp
  where ename in ( 'BLAKE','JONES' )
        ) x,
        t500
  where t500.id <= blake_hd-jones_hd+1;

SQL Server:使用透视表 T500 生成足够的行数,用于表示两个日期之间的每一天。再计算不是周末的天数。使用函数DATENAME 确定各个日期是星期几。

sql 复制代码
 select sum(case when datename(dw,jones_hd+t500.id-1)
                   in ( 'SATURDAY','SUNDAY' )
                 then 0 else 1
            end) as days
   from (
 selectmax(case when ename = 'BLAKE'
                 then hiredate
            end) as blake_hd,
        max(case when ename = 'JONES'
                 then hiredate
            end) as jones_hd
   from emp
  where ename in ( 'BLAKE','JONES' )
        ) x,
        t500
  where t500.id <= datediff(day,jones_hd-blake_hd)+1;

4、确定两个日期相隔多少个月或多少年

问题:你想确定两个日期相隔多少个月或多少年。例如,你想找出第一位员工和最后一位员工获聘的日期相隔多少个月,还想确定这段时间相当于多少年。

解决方案:由于一年是 12 个月,因此找出两个日期相隔多少个月后,只要再除以 12 就可以得到相隔多少年。熟悉本解决方案后,你可能想根据需要对相差的年数进行舍入处理。

例如,在 EMP 表中,第一个 HIREDATE 为 1980 年 12月 17 日,最后一个 HIREDATE 为 1983 年 1 月 12 日。如果只考虑年份,那么结果将为 3(1983 -- 1980)年,但相差的月数大约为 25(两年多一点儿)​。应该根据需要调整解决方案。下面的解决方案都返回 25 个月和大约两年。

DB2 和 MySQL:使用函数 YEAR 和 MONTH 返回指定日期的 4 位年份和 2位月份。

sql 复制代码
select mnth, mnth/12
from (
	select (year(max_hd) - year(min_hd))*12 + (month(max_hd) - month(min_hd)) as mnth
	from (
		select min(hiredate) as min_hd, max(hiredate) as max_hd
		from emp
	) x
) y;

Oracle:使用函数 MONTHS_BETWEEN 确定两个日期相隔多少个月(要确定相隔多少年,只需再除以 12)​。

sql 复制代码
select months_between(max_hd,min_hd),
       months_between(max_hd,min_hd)/12
  from (
select min(hiredate) min_hd, max(hiredate) max_hd
  from emp
       ) x;

PostgreSQL:使用函数 EXTRACT 返回指定日期的 4 位年份和 2 位月份。

sql 复制代码
SELECT mnth,
       mnth / 12 AS years
FROM (
    SELECT (EXTRACT(YEAR FROM max_hd) - EXTRACT(YEAR FROM min_hd)) * 12
         + (EXTRACT(MONTH FROM max_hd) - EXTRACT(MONTH FROM min_hd)) AS mnth
    FROM (
        SELECT MIN(hiredate) AS min_hd,
               MAX(hiredate) AS max_hd
        FROM emp
    ) x
) y;

SQL Server:使用函数 DATEDIFF 确定两个日期相隔多长时间,并使用参数 datepart 将时间单位指定为月或年。

sql 复制代码
select datediff(month,min_hd,max_hd),
       datediff(year,min_hd,max_hd)
  from (
select min(hiredate) min_hd, max(hiredate) max_hd
  from emp
       ) x;

5、确定两个日期相隔多少秒、多少分钟或多少小时

问题:你想返回两个日期相隔的秒数。例如,你想返回 ALLEN和 WARD 的 HIREDATE 相隔的秒数、分钟数和小时数。

解决方案:只要能确定两个日期相隔多少天,就能确定它们相隔多少秒、多少分钟和多少小时,因为它们只是不同的时间单位而异。

DB2:使用函数 DAYS 确定 ALLEN_HD 和 WARD_HD 相隔多少天,再使用乘法运算计算小时数、分钟数和秒数。

sql 复制代码
 select dy*24 hr, dy*24*60 min, dy*24*60*60 sec
   from (
 select ( days(max(case when ename = 'WARD'
                   then hiredate
              end)) -
          days(max(case when ename = 'ALLEN'
                   then hiredate
              end))
        ) as dy
   from emp
        ) x;

MySQL:使用函数 DATEDIFF 返回 ALLEN_HD 和 WARD_HD 相隔多少天,再使用乘法运算计算小时数、分钟数和秒数。

sql 复制代码
select datediff(day,allen_hd,ward_hd)*24 hr,
	datediff(day,allen_hd,ward_hd)*24*60 min,
	datediff(day,allen_hd,ward_hd)*24*60*60 sec
from (
	select max(case when ename = 'WARD'
					then hiredate
					end) as ward_hd,
				 max(case when ename = 'ALLEN'
					then hiredate
					end) as allen_hd
	from emp
) x;

SQL Server:使用函数 DATEDIFF 返回 ALLEN_HD 和 WARD_HD 相隔多少天,再使用参数 DATEPART 指定时间单位。

sql 复制代码
 select datediff(day,allen_hd,ward_hd,hour) as hr,
        datediff(day,allen_hd,ward_hd,minute) as min,
        datediff(day,allen_hd,ward_hd,second) as sec
   from (
 select max(case when ename = 'WARD'
                  then hiredate
            end) as ward_hd,
        max(case when ename = 'ALLEN'
                 then hiredate
            end) as allen_hd
   from emp
        ) x;

Oracle 和 PostgreSQL:使用减法运算返回 ALLEN_HD 和 WARD_HD 相隔多少天,再使用乘法运算计算小时数、分钟数和秒数。

sql 复制代码
select dy*24 as hr, dy*24*60 as min, dy*24*60*60 as sec
from (
	select (max(case when ename = 'WARD'
					then hiredate
					end) -
					max(case when ename = 'ALLEN'
					then hiredate
					end)) as dy
	from emp
) x;

 hr | min  |  sec   
----+------+--------
 48 | 2880 | 172800
(1 row)

6、计算一年中有多少个工作日

问题:你想计算一年中有多少个工作日(同 8.3 节,在本实例中,只要不是星期六或星期日,就认为是工作日)​。

解决方案:为了确定一年中有多少个工作日,必须做到以下几点。

  • 生成这一年内所有的日期。
  • 设置这些日期的格式,确定它们是星期几。
  • 计算有多少个工作日。

DB2:使用递归式 WITH,以免使用 SELECT 来查询一张至少包含 366 行的表。使用函数 DAYNAME 确定各个日期是星期几,然后计算每个工作日出现的次数。

sql 复制代码
 with x (start_date,end_date)
 as (
 select start_date,
        start_date + 1 year end_date
   from (
 select (current_date
         dayofyear(current_date) day)
         +1 day as start_date
   from t1
        ) tmp
   union all
 select start_date + 1 day, end_date
   from x
  where start_date + 1 day < end_date
 )
 select dayname(start_date),count(*)
   from x
  group by dayname(start_date);

MySQL:查询 T500 表以生成足够的行,从而返回一年中的每一天。使用函数 DATE_FORMAT 确定各个日期都是星期几,然后计算工作日出现的次数。

sql 复制代码
select date_format(
	date_add(
		cast(concat(year(current_date),'-01-01') as date),
		interval t500.id-1 day),'%W') day,
	count(*)
from t500
where t500.id <= datediff(
	cast(concat(year(current_date)+1,'-01-01') as date),
	cast(concat(year(current_date),'-01-01') as date))
group by date_format(
	date_add(
		cast(concat(year(current_date),'-01-01') as date),
		interval t500.id-1 day),'%W');

Oracle:可以使用递归式 CONNECT BY 返回一年中的每一天。

sql 复制代码
 with x as (
 select level lvl
   from dual
  connect by level <= (
    add_months(trunc(sysdate,'y'),12)-trunc(sysdate,'y')
  )
 )
 select to_char(trunc(sysdate,'y')+lvl-1,'DAY'), count(*)
   from x
  group by to_char(trunc(sysdate,'y')+lvl-1,'DAY');

PostgreSQL:首先使用内置函数 GENERATE_SERIES 为一年中的每一天都生成一行数据,然后使用函数 TO_CHAR 确定各个日期都是星期几,最后计算工作日出现的次数。

sql 复制代码
select to_char(
	cast( date_trunc('year',current_date) as date) + gs.id-1,'DAY'),
	count(*)
from generate_series(1,366) gs(id)
where gs.id <= (
	cast(date_trunc('year',current_date) +interval '12 month' as date) -
	cast(date_trunc('year',current_date) as date))
group by to_char(
	cast(date_trunc('year',current_date)as date) + gs.id-1,'DAY');

  to_char  | count 
-----------+-------
 MONDAY    |    52
 TUESDAY   |    52
 SUNDAY    |    52
 WEDNESDAY |    52
 THURSDAY  |    53
 FRIDAY    |    52
 SATURDAY  |    52
(7 rows)

SQL Server:使用递归式 WITH,以免使用 SELECT 来查询一个至少包含 366 行的表。使用函数 DATENAME 确定各个日期都是星期几,然后计算工作日出现的次数。

sql 复制代码
 with x (start_date,end_date)
 as (
 select start_date,
        dateadd(year,1,start_date) end_date
   from (
 select cast(
        cast(year(getdate()) as varchar) + '-01-01'
             as datetime) start_date
   from t1
        ) tmp
 union all
 select dateadd(day,1,start_date), end_date
   from x
  where dateadd(day,1,start_date) < end_date
 )
 select datename(dw,start_date),count(*)
   from x
  group by datename(dw,start_date)
 OPTION (MAXRECURSION 366);

7、确定当前记录和下一条记录存储的日期相隔多少天

问题:你想确定两个日期(准确地说是存储在两行中的两个日期)相差多少天。例如,对于 10 号部门的每一位员工,你想确定从其获聘起到下一位员工(可能是另一个部门的)获聘过了多少天。

解决方案:要解决这个问题,诀窍是找出当前员工的 HIREDATE 后最早的 HIREDATE,然后,使用 2 节介绍的技巧确定相隔的天数即可。

DB2:使用标量子查询找出当前 HIREDATE 后的第一个HIREDATE,然后使用函数 DAYS 计算这两个日期相差的天数。

sql 复制代码
select x.*,
       days(x.next_hd) - days(x.hiredate) diff
  from (
select e.deptno, e.ename, e.hiredate,
       lead(hiredate)over(order by hiredate) next_hd
  from emp e
 where e.deptno = 10
       ) x;

MySQL 和 SQL Server:使用函数 LEAD 访问下一条记录。下面演示了在 SQLServer 解决方案中如何使用函数 DATEDIFF。

sql 复制代码
select x.ename, x.hiredate, x.next_hd,
       datediff(x.hiredate,x.next_hd,day) as diff
  from (
select deptno, ename, hiredate,
       lead(hiredate)over(order by hiredate) as next_hd
  from emp e
       ) x
 where e.deptno=10;

Oracle: 使用窗口函数 LEAD OVER 访问按照 HIREDATE 排序时位于当前行后面一行的 HIREDATE,然后执行减法运算。

sql 复制代码
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;

PostgreSQL:使用标量子查询找出当前 HIREDATE 后的第一个HIREDATE,然后使用减法运算找出相隔的天数。

sql 复制代码
select x.*,
       x.next_hd - x.hiredate as diff
  from (
select e.deptno, e.ename, e.hiredate,
       lead(hiredate)over(order by hiredate) as next_hd
  from emp e
 where e.deptno = 10
       ) x;