【问题标题】:Oracle, SQL, how to get intervals between datesOracle,SQL,如何获取日期之间的间隔
【发布时间】:2017-11-27 09:39:42
【问题描述】:

我需要帮助解决一个问题。其实不知道能不能直接用SQL解决。

我有一份作品清单。每部作品都有开始日期和结束日期,格式如下

YYYY/MM/DD  HH24:MI:SS

我需要计算这些工作的成本,小时价格取决于工作完成的时间间隔:

Nigth time: 22:00 to 6:00, for example: 20 €/h
Normal time: the rest 17 €/h

所以,如果我有这样的样本:

wo     start                 end
21    2017/11/16 21:25:00    2017/11/16 22:55:00
22    2017/11/17 05:45:00    2017/11/17 07:05:00
23    2017/11/18 23:00:00    2017/11/19 1:10:00
24    2017/11/17 18:00:00    2017/11/17 19:00:00

我需要计算 22h 和 6h 之间的日期间隔,然后将它们乘以相应的价格

wo     rest(minutes)   night(minutes)
21      35              55
22      15              65
23       0              130
24       1               0

提前感谢您的帮助。

【问题讨论】:

  • 对于wo - 23,end 的日期部分不应该是 2017/11/19 而不是 2017/11/18?
  • 是的。已编辑。谢谢。

标签: sql oracle timestamp intervals


【解决方案1】:

呵呵。如果你真的想要的话:)

已添加第五条记录(从 2016 年 10 月 30 日开始)用于测试目的。

SQL> with
  2    src as (select timestamp '2017-11-16 21:25:00' b, timestamp '2017-11-16 22:55:00' f from dual union all
  3            select timestamp '2017-11-17 05:45:00' b, timestamp '2017-11-17 07:05:00' f from dual union all
  4            select timestamp '2017-11-18 23:00:00' b, timestamp '2017-11-19 1:10:00' f from dual union all
  5            select timestamp '2017-11-17 18:00:00' b, timestamp '2017-11-17 19:00:00' f from dual union all
  6            select timestamp '2016-10-30 00:00:00' b, timestamp '2016-11-03 23:00:00' f from dual),
  7    srd as (select b, f, f - b t from src),
  8    mmm as (select min(trunc(b)) b, max(trunc(f)) f from src),
  9    rws as (select b + 6/24 + rownum - 1 b, b + 22/24 + rownum - 1 f from mmm connect by level <= f - b + 1),
 10    mix as (select s.b, s.f, s.t, r.b rb, r.f rf from srd s, rws r where s.f >= r.b (+) and r.f (+) >= s.b),
 11    clc as (select b, f, t, nvl(numtodsinterval(sum((least(f, rf) + 0) - (greatest(b, rb) + 0)), 'DAY'), interval '0' second) d from mix group by b, f, t)
 12  select
 13    to_char(b, 'dd.mm.yyyy hh24:mi') as "datetime begin",
 14    to_char(f, 'dd.mm.yyyy hh24:mi') as "datetime finish",
 15    cast(t as interval day to second(0)) as "total time",
 16    cast(d as interval day to second(0)) as "daytime",
 17    cast(t - d as interval day to second(0)) as "nighttime"
 18  from
 19    clc
 20  order by
 21    1, 2;

datetime begin     datetime finish    total time     daytime        nighttime
------------------ ------------------ -------------- -------------- --------------
16.11.2017 21:25   16.11.2017 22:55   +00 01:30:00   +00 00:35:00   +00 00:55:00
17.11.2017 05:45   17.11.2017 07:05   +00 01:20:00   +00 01:05:00   +00 00:15:00
17.11.2017 18:00   17.11.2017 19:00   +00 01:00:00   +00 01:00:00   +00 00:00:00
18.11.2017 23:00   19.11.2017 01:10   +00 02:10:00   +00 00:00:00   +00 02:10:00
30.10.2016 00:00   03.11.2016 23:00   +04 23:00:00   +03 08:00:00   +01 15:00:00

【讨论】:

  • 不幸的是,Oracle 没有通用的日期范围运算符/函数,这使得它变得更加困难和混乱。 Postgresql 的人已经理解了这个缺点,并包含了范围数据类型和重叠运算符。
  • @KaushikNayak:我希望微软在 SQL Server 中也能提供这样的功能,找到重叠的日期/时间确实需要一些多语句工作。
  • 范围运算符是小菜一碟。它存在——太棒了,它不存在——也很好。如图所示,没有合理 API 的日期 + 时间戳 + 间隔数据类型的难点问题要困难得多。
【解决方案2】:

另一种方法更蛮力,但它允许将间隔配置与报告区分开来。 它分三步进行:

1) 定义一天中每分钟的费率类型(如果需要,更改粒度)

create table day_config as
with helper as (
select 
rownum -1 minute_id 
from dual connect by level <= 24*60),
helper2 as (
select 
minute_id,
trunc(minute_id/60) hour_no,
mod(minute_id,60) minute_no
from helper) 
select
minute_id,hour_no, minute_no,
case when hour_no >= 22 or hour_no <= 5 then 0 else 1 end rate_id
from helper2;

select * from day_config order by minute_id;

 MINUTE_ID    HOUR_NO  MINUTE_NO    RATE_ID
---------- ---------- ---------- ----------
         0          0          0          0 
         1          0          1          0 
         2          0          2          0 
         3          0          3          0 
         4          0          4          0 
         5          0          5          0 
         6          0          6          0 
         7          0          7          0 
         8          0          8          0 
         9          0          9          0 

这里 rate_id 表示晚上,rate_id 1 表示一天。 优点是,您可以根据需要引入尽可能多的费率类型。

2) 扩展所需间隔的配置,例如到一整年。 所以现在我们有了一年中每一分钟的配置,即要应用的速率。

create or replace view year_config as
select my_date + MINUTE_ID / (24*60) minute_ts , MINUTE_ID, HOUR_NO, MINUTE_NO, RATE_ID from day_config 
cross join 
(select DATE '2017-01-01' + rownum -1 as my_date from dual connect by level <= 365)
order by 1,2;

select * from (
select * from year_config
order by 1)
where rownum <= 5;

MINUTE_TS            MINUTE_ID    HOUR_NO  MINUTE_NO    RATE_ID
------------------- ---------- ---------- ---------- ----------
01-01-2017 00:00:00          0          0          0          0 
01-01-2017 00:01:00          1          0          1          0 
01-01-2017 00:02:00          2          0          2          0 
01-01-2017 00:03:00          3          0          3          0 
01-01-2017 00:04:00          4          0          4          0

3) 报告就像加入我们的配置表一样简单,限制间隔(半开)并在 RATE 中分组。

select b, f,RATE_ID, count(*) minute_cnt 
from tst join year_config c on c.MINUTE_TS >= tst.b and c.MINUTE_TS < tst.f 
group by b, f,RATE_ID
order by b, f,RATE_ID;

B                   F                      RATE_ID MINUTE_CNT
------------------- ------------------- ---------- ----------
16-11-2017 21:25:00 16-11-2017 22:55:00          0         55 
16-11-2017 21:25:00 16-11-2017 22:55:00          1         35 
17-11-2017 05:45:00 17-11-2017 07:05:00          0         15 
17-11-2017 05:45:00 17-11-2017 07:05:00          1         65 
17-11-2017 18:00:00 17-11-2017 19:00:00          1         60 
18-11-2017 23:00:00 19-11-2017 01:10:00          0        130

【讨论】:

    【解决方案3】:

    最简单的方法可能是在递归WITH 子句中计算所有分钟数,然后查看分钟数落在哪个时间范围内。不幸的是,由于 Oracle 没有 TIME 数据类型,我们将不得不使用时间字符串('00'00' 直到 '23:59')。

    with shifts as
    (
      select 'night' as shift,  '00:00' as starttime, '05:59' as endtime, 20 as cost from dual
      union all
      select 'normal' as shift, '06:00' as starttime, '21:59' as endtime, 17 as cost from dual
      union all
      select 'night' as shift,  '22:00' as starttime, '23:59' as endtime, 20 as cost from dual
    )
    , workminutes(wo, workminute, thetime, endtime) as
    (
      select wo, to_char(starttime, 'hh24:mi') as workminute, starttime as thetime, endtime
      from mytable
      union all
      select 
        wo, 
        to_char(thetime + interval '1' minute, 'hh24:mi') as workminute,
        thetime + interval '1' minute as thetime,
        endtime
      from workminutes
      where thetime + interval '1' minute < endtime
    )
    select 
      wo, 
      count(case when s.shift = 'normal' then 1 end) as normal_time,
      coalesce(sum(case when m.workminute between '06:00' and '21:59' then s.cost end), 0)
        as normal_cost,
      count(case when s.shift = 'night' then 1 end) as night_time,
      coalesce(sum(case when m.workminute not between '06:00' and '21:59' then s.cost end), 0)
        as night_cost,
      count(*) as total_time,
      coalesce(sum(s.cost), 0)
        as total_cost
    from workminutes m
    join shifts s on m.workminute between s.starttime and s.endtime
    group by wo
    order by wo;
    

    输出:

    WO NORMAL_TIME NORMAL_COST NIGHT_TIME NIGHT_COST TOTAL_TIME TOTAL_COST 21 35 595 55 1100 90 1695 22 65 1105 15 300 80 1405 23 0 0 130 2600 130 2600 24 60 1020 0 0 60 1020 25 4800 81600 2340 46800 7140 128400

    (当然,如果您有一个真正的班次表并且不必即时制作一个班次表,那么此查询当然看起来要好得多。此外,您可能不需要我的结果中的所有这七列.)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-01-06
      • 1970-01-01
      • 2019-11-02
      • 1970-01-01
      • 1970-01-01
      • 2018-10-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多