从一个SQL打印全年日历漫谈数据仓库中时间操作场景的重点写法

Posted 赵延东的一亩三分地

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从一个SQL打印全年日历漫谈数据仓库中时间操作场景的重点写法相关的知识,希望对你有一定的参考价值。

文章目录


前言

在本月工作及与网友互动的SQL开发问题中,大家经常会问到时间处理的问题,比如下面几个问题就是大家最常问问题:

  1. 我如何快速确定今年是否是闰年的😣
  2. 我如何从DATE类型数据获取年、月(月初&月末)、周、日、时、分、秒信息🤯
  3. 我如何快速查到本月月初第一周的周一和本月最后一周周一是在几号😑
  4. 我如何快速确定每个季度的开始和结束日期😫
  5. 领导让统计每个月招聘的人数,但是有的月份没招人也就没在数据库记录当月招聘信息,我怎么统计出来没有的月份😤
  6. 领导让每隔十分钟统计一次数据库登录人数,我该怎么统计啊🙄
  7. 突发奇想,大佬你能否用一条sql打印一年的日历出来,我懵逼了😫😭😤🤯
    就上面这些问题我统一给大家通过一些案例去讲解如何去实现这些需求。

一、我如何快速确定今年是否是闰年的😣

闰年(Leap Year)共有366天(1-12月分别为31天,29天,31天,30天,31天,30天,31天,31天,30天,31天,30天,31天),是为了弥补因人为历法规定造成的年度天数与地球实际公转周期的时间差而设立的,闰年又分为普通闰年和世纪闰年:

  • 普通年判断方法:能被4整除且不能被100整除的为闰年(如2004年就是闰年,1999年不是闰年)。
  • 世纪年判断方法:能被400整除的是闰年(如2000年是闰年,1900年不是闰年)。
    看到上面的介绍大家应该对闰年有了一定了解,那怎么在数仓中用SQL快速判断某一年是否是闰年呢???
    其实若要判断一年是否为闰年,只需要看二月的月末是哪一天就可以啦!!!
SQL> select trunc(sysdate, 'y') as 年初,
  2         add_months(trunc(sysdate, 'y'), 1) as 二月初,
  3         last_day(add_months(trunc(sysdate, 'y'), 1)) as 二月底,
  4         to_char(last_day(add_months(trunc(sysdate, 'y'), 1)), 'dd') as5    from dual;

年初        二月初      二月底      日
----------- ----------- ----------- ---------------------------------------------------------------------------
2023-1-1    2023-2-1    2023-2-28   28

从上面结果立马知道了,今年是平年,这么计算是不是超级简单???

二、 我如何从DATE类型数据获取年、月(月初&月末)、周、日、时、分、秒信息🤯

经常看到有人因为不熟悉日期操作,获取相应信息的时候,要写很复杂的语句。下面举一个简单的例子。

SQL> select hiredate,
  2         to_date(to_char(hiredate, 'yyyy-mm') || '-1', 'yyyy-mm-dd') as yuechu
  3    from emp
  4   where rownum <= 1;

HIREDATE    YUECHU
----------- -----------
1980-12-17  1980-12-1

SQL> 

其实要获取这个数据,只需要一个简单的函数就可以做到,而根本不需要多次转换:

SQL> SELECT hiredate AS 雇佣日期, trunc(hiredate, 'mm') AS 月初
  2    from emp
  3   where rownum <= 1;

雇佣日期    月初
----------- -----------
1980-12-17  1980-12-1

SQL> 

下面列举几个常用的取值方式,希望对大家有用。

SQL> SELECT hiredate,
  2  to_number(to_char(hiredate,'hh24'))时,
  3  to_number(to_char(hiredate,'mi')),
  4  to_number(to_char(hiredate,'ss'))秒,
  5  to_number(to_char(hiredate,'dd')),
  6  to_number(to_char(hiredate,'mm')),
  7  to_number(to_char(hiredate,'yyyy')),
  8  to_number(to_char(hiredate,'ddd'))年内第几天,
  9  trunc(hiredate,'dd')一天之始,
 10  trunc(hiredate,'day')周初,
 11  trunc(hiredate,'dy')周初,
 12  trunc(hiredate,'mm')月初,
 13  last_day(hiredate)月未,
 14  add_months(trunc(hiredate,'mm'),1)下月初,
 15  trunc(hiredate,'yy')年初,
 16  to_char(hiredate,'day')周几,
 17  to_char(hiredate,'dy')周几,
 18  to_char(hiredate,'month')月份,
 19  to_char(hiredate,'mm')月份
 20  FROM(SELECT hiredate+30/24/60/60+20/24/60+5/24 AS hiredate FROM emp WHERE ROWNUM<=1);

HIREDATE             时          分          秒          日          月          年      年内第几天 一天之始    周初        周初        月初        月未        下月初      年初        周几                                                                        周几                                                                        月份                                                                        月份
----------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------- ----------- ----------- ----------- ----------- ----------- ----------- --------------------------------------------------------------------------- --------------------------------------------------------------------------- --------------------------------------------------------------------------- ---------------------------------------------------------------------------
1980-12-17           5         20         30         17         12       1980        352 1980-12-17  1980-12-14  1980-12-14  1980-12-1   1980-12-31  1981-1-1    1980-1-1    星期三                                                                      星期三                                                                      1212

SQL> 

三、我如何快速查到本月月初第一周的周一和本月最后一周周一是在几号😑

这个问题是一个后端JAVA开发网友问我的,我先给大家分析一下我的思路:
首先分析计算月初第一个周一,无非是两种情况,拿本月(2月)来做案例:

  • 本月的第一个周一恰好是本月1号;
  • 本月的第一个周一可能是在3号,因为这个月1,2号是上个月最后一周的周六、周日;

其次是计算本月最后一个周一,无非也是两种情况:

  • 最后一个周一恰好是月末最后一天;
  • 最后一个周一是月末的前几天,但是肯定是当月最后一天的前七天内;

上面两个分析逻辑,总结一句话:
其实计算当月内第一个星期一与最后一个星期一,我们只需要分别找上月末及当月末之前七天的下一周周一即可。

SQL> select next_day(trunc(sysdate,'mm')-1,2) as 第一周周一,
  2  next_day(last_day(trunc(sysdate,'mm'))-7,2) as 最后一周的周一
  3  from dual;

第一周周一  最后一周的周一
----------- -----------
2023-2-6    2023-2-27

当我给他这个sql的时候,他很是震惊,因为他们团队的开发DBA给他的是枚举SQL,写的很长!!!

四、我如何快速确定每个季度的开始和结束日期😫

这个问题,是几个做报表的网友问我的,生成汇总报表时候时常要求按季度分类汇总这就需要通过给定年份提取对应的季度信息,那怎么快速获取某个季度的开始结束日期呢?
我是这样给他的:

SQL> with t as
  2   (select to_char(sysdate, 'yyyy') as, level as sn
  3      from dual
  4    connect by level <= 4
  5    )
  6  select sn as 季度,
  7         (sn - 1) * 3 + 1 as 开始月份,
  8         add_months(trunc(to_date(, 'yyyy'), 'y'), (sn - 1) * 3) as 开始日期,
  9         add_months(trunc(to_date(, 'yyyy'), 'y'), sn * 3) - 1 as 结束日期
 10    from t;

        季度       开始月份 开始日期    结束日期
---------- ---------- ----------- -----------
         1          1 2023-1-1    2023-3-31
         2          4 2023-4-1    2023-6-30
         3          7 2023-7-1    2023-9-30
         4         10 2023-10-1   2023-12-31

SQL> 	

其实只要想清楚计算公式和日期的计算方法,就可以快速计算出来了。

五、领导让统计每个月招聘的人数,但是有的月份没招人也就没在数据库记录当月招聘信息,我怎么统计出来没有的月份😤

有时业务数据并不是连续的,比如考勤、比如网友说的招聘信息,可能在某一天或则某一月并没有相关操作,数据库也就没相关记录,而领导却要展示报表时候,这些日期的信息项都要展示出来用于后续分析统计,那我们该怎么统计呢?接下来用一个案例来一步步解释!
现在有下面员工信息

SQL> select empno,hiredate from emp order by 2;

EMPNO HIREDATE
----- -----------
 7369 1980-12-17
 7499 1981-2-20
 7521 1981-2-22
 7566 1981-4-2
 7698 1981-5-1
 7782 1981-6-9
 7844 1981-9-8
 7654 1981-9-28
 7839 1981-11-17
 7900 1981-12-3
 7902 1981-12-3
 7934 1982-1-23
 7788 1987-4-19
 7876 1987-5-23
 1001 2021-10-9 1

15 rows selected

有的年份没有招聘员工,这时按年份查询招聘人数结果如下:

SQL> select to_char(hiredate,'yyyy') as year, count(*) as cnt
  2  from emp
  3  group by to_char(hiredate,'yyyy')
  4  order by 1;

YEAR                                                                               CNT
--------------------------------------------------------------------------- ----------
1980                                                                                 1
1981                                                                                10
1982                                                                                 1
1987                                                                                 2
2021                                                                                 1

SQL> 

为了分析数据一般需要把表中没有的年份(如1983年)内的人数统计为0,这时就需要先根据表中的信息生成一个年份的枚举列表。

SQL> with t as
  2   (select extract(year from min(hiredate)) as 开始年份,
  3           extract(year from max(hiredate)) as 结束年份
  4      from emp where empno<>1001)
  5  select 开始年份 + (level - 1) as 年份
  6    from t
  7  connect by level <= ((结束年份 - 开始年份) + 1);

        年份
----------
      1980
      1981
      1982
      1983
      1984
      1985
      1986
      1987

8 rows selected

通过这个列表关联查询就可以得到所有年份的数据。

SQL> with t as
  2   (select extract(year from min(hiredate)) as 开始年份,
  3           extract(year from max(hiredate)) as 结束年份
  4      from emp
  5     where empno <> 1001),
  6  t1 as
  7   (select 开始年份 + (level - 1) as 年份
  8      from t
  9    connect by level <= ((结束年份 - 开始年份) + 1))
 10  select t1.年份, count(emp.empno) as 聘用人数
 11    from emp
 12   right join t1
 13      on (extract(year from emp.hiredate) = t1.年份)
 14   group by 年份
 15   order by 1;

        年份       聘用人数
---------- ----------
      1980          1
      1981         10
      1982          1
      1983          0
      1984          0
      1985          0
      1986          0
      1987          2

8 rows selected

上面这种枚举关联处理方式,适用于很多场景。

六、领导让每隔十分钟统计一次数据库登录人数,我该怎么统计啊🙄

这也是一个做数据分析的网友请教的问题,他提的问题是:按指定的时间间隔(10分钟)汇总数据,分别汇总0分、10分、20分、30分等。
我这里用v$sql表来当作案例数据,先看下这里的部分数据:

select a.LAST_ACTIVE_TIME from v$sql a where rownum<=5;
LAST_ACTIVE_TIME
----------------
2023-2-13 22:14:27
2023-2-15 3:00:59
2023-2-7 1:05:29
2023-2-8 1:05:30
2023-2-15 15:59:03
我们一步步来。

1、截取数据到分钟,并提取分钟信息

select sql_id,trunc(a.LAST_ACTIVE_TIME,'mi') as tim,to_char(a.LAST_ACTIVE_TIME,'mi') as mi  from v$sql a where sql_id='gcsnqzu9q0004'
SQL_ID	TIM	MI
gcsnqzu9q0004	2023-2-13 22:14:00	14

2、对14和10取余

SQL> select mod(14,10) from dual;

MOD(14,10)
----------
         4

SQL> 

3、对比上面结果,我们可以知道如果想计算整10分钟的间隔,那就直接用MI-MOD(14,10)就算出来这个分钟对应在整十的哪个范围了。

select sql_id,
       trunc(a.LAST_ACTIVE_TIME, 'mi') as tim,
       to_char(a.LAST_ACTIVE_TIME, 'mi') as mi,
       to_char(a.LAST_ACTIVE_TIME, 'mi') -
       mod(to_char(a.LAST_ACTIVE_TIME, 'mi'), 10) as new_mi
  from v$sql a
 where sql_id = 'gcsnqzu9q0004';
SQL_ID	TIM	MI	NEW_MI
gcsnqzu9q0004	2023-2-13 22:14:00	14	10

那么铺垫做完了,这个需求的最终实现sql如下:

SQL> with t as
  2   (select sql_id,
  3           trunc(a.LAST_ACTIVE_TIME, 'mi') -
  4           mod(to_char(a.LAST_ACTIVE_TIME, 'mi'), 10) / 24 / 60 as new_tim
  5      from v$sql a)
  6      select * from (
  7  select new_tim, count(*) from t group by new_tim  order by new_tim desc nulls last
  8  )where rownum<=10;

NEW_TIM       COUNT(*)
----------- ----------
2023-2-15 19:00:00	476
2023-2-15 18:50:00	44
2023-2-15 18:40:00	20
2023-2-15 18:30:00	21
2023-2-15 18:20:00	52
2023-2-15 18:10:00	4
2023-2-15 18:00:00	7
2023-2-15 17:50:00	2
2023-2-15 17:40:00	7
2023-2-15 17:30:00	2


10 rows selected


SQL> 

七、突发奇想,大佬你能否用一条sql打印一年的日历出来,我懵逼了😫😭😤🤯

大家看到这个需求会不会有点懵逼?用SQL竟然能打印一张全年的日历??
其实我们可以枚举所有月份所有的日期,并转换为对应的月、周信息,再按所在周做一次“行转列”即可。
我们先来了解一下Oracle的日期转换函数的使用小案例:

SQL> WITH x AS
  2   (SELECT to_date('2013-12-27', 'yyyy-mm-dd') + (LEVEL - 1) AS d
  3      FROM dual
  4    CONNECT BY LEVEL <= 5)
  5  SELECT d, to_char(d, 'day') AS DAY, to_char(d, 'iw') AS iw FROM x;

D           DAY                                                                         IW
----------- --------------------------------------------------------------------------- ---------------------------------------------------------------------------
2013-12-27  星期五                                                                      52
2013-12-28  星期六                                                                      52
2013-12-29  星期日                                                                      52
2013-12-30  星期一                                                                      01
2013-12-31  星期二                                                                      01

SQL> 

我们从上面获得了周信息与周对应年中属于第几周,这里有一个小问题,2013-12-30那一周应该是第53周的,却被算到了第二年的第一周。
这种数据需要用case when来处理。

SQL> 
SQL> WITH x AS
  2   (SELECT to_date('2013-12-27', 'yyyy-mm-dd') + (LEVEL - 1) AS d
  3      FROM dual
  4    CONNECT BY LEVEL <= 5),
  5  x1 as
  6   (SELECT d,
  7           to_char(d, 'day') AS DAY,
  8           to_char(d, 'mm') AS mm,
  9           to_char(d, 'iw') AS iw
 10      FROM x)
 11  select d,
 12         day,
 13         mm,
 14         iw,
 15         case
 16           when mm = 12 and以上是关于从一个SQL打印全年日历漫谈数据仓库中时间操作场景的重点写法的主要内容,如果未能解决你的问题,请参考以下文章

SQL开发实战技巧系列(十六):数据仓库中时间类型操作(初级)日月年时分秒之差及时间间隔计算

SQL开发实战技巧系列(十八):数据仓库中时间类型操作(进阶)INTERVALEXTRACT以及如何确定一年是否为闰年及周的计算

2021年日历表打印版,2021年日历表

2021年日历全年表一张图,2021年的年历怎么做又漂亮

Calendar 日历类的时间操作

漫谈 | 大牛带你从0到1构建数据仓库实战