简体   繁体   English

在 Oracle SQL 中根据营业时间计算小时数

[英]calculate hours based on business hours in Oracle SQL

I am looking to calculate hours between a start and end of time of a task based on business hours.我希望根据工作时间计算任务开始时间和结束时间之间的时间。 I have the following sample data:我有以下示例数据:

TASK | START_TIME | END_TIME
A | 16-JAN-17 10:00 | 23-JAN-17 11:35
B | 18-JAN-17 17:53 | 19-JAN-17 08:00
C | 13-JAN-17 13:00 | 17-JAN-17 14:52
D | 21-JAN-17 10:00 | 30-JAN-17 08:52

and I need to work out the difference between the two but based on the following business hours:我需要根据以下工作时间计算出两者之间的差异:

Mon - Sat 08:00 - 18:00

I know how to write the calculation but not sure what I do to add the business hours into the calculation.我知道如何编写计算,但不确定如何将营业时间添加到计算中。

Any advice would be appreciated.任何建议将不胜感激。

You can directly calculate the difference in hours:您可以直接计算小时数的差异:

SELECT task,
       start_time,
       end_time,
       ROUND(
         (
           -- Calculate the full weeks difference from the start of ISO weeks.
           ( TRUNC( end_time, 'IW' ) - TRUNC( start_time, 'IW' ) ) * (10/24) * (6/7)
           -- Add the full days for the final week.
           + LEAST( TRUNC( end_time ) - TRUNC( end_time, 'IW' ), 6 ) * (10/24)
           -- Subtract the full days from the days of the week before the start date.
           - LEAST( TRUNC( start_time ) - TRUNC( start_time, 'IW' ), 6 ) * (10/24)
           -- Add the hours of the final day
           + LEAST( GREATEST( end_time - TRUNC( end_time ) - 8/24, 0 ), 10/24 )
           -- Subtract the hours of the day before the range starts.
           - LEAST( GREATEST( start_time - TRUNC( start_time ) - 8/24, 0 ), 10/24 )
         )
         -- Multiply to give minutes rather than fractions of full days.
         * 24,
         15 -- Number of decimal places
       ) AS work_day_hours_diff
FROM   your_table;

Which, for your sample data:其中,对于您的示例数据:

CREATE TABLE your_table ( TASK, START_TIME, END_TIME ) AS
SELECT 'A', DATE '2017-01-16' + INTERVAL '10:00' HOUR TO MINUTE, DATE '2017-01-23' + INTERVAL '11:35' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'B', DATE '2017-01-18' + INTERVAL '17:53' HOUR TO MINUTE, DATE '2017-01-19' + INTERVAL '08:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'C', DATE '2017-01-13' + INTERVAL '13:00' HOUR TO MINUTE, DATE '2017-01-17' + INTERVAL '14:52' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'D', DATE '2017-01-21' + INTERVAL '10:00' HOUR TO MINUTE, DATE '2017-01-30' + INTERVAL '08:52' HOUR TO MINUTE FROM DUAL;

Outputs (with the date format YYYY-MM-DD HH24:MI:SS (DY) ):输出(日期格式YYYY-MM-DD HH24:MI:SS (DY) ):

\nTASK |任务 | START_TIME | START_TIME | END_TIME | END_TIME | WORK_DAY_HOURS_DIFF WORK_DAY_HOURS_DIFF\n:--- | :--- | :------------------------ | :------------------------ | :------------------------ | :------------------------ | ------------------: ------------------:\nA |一个 | 2017-01-16 10:00:00 (MON) | 2017-01-16 10:00:00 (星期一) | 2017-01-23 11:35:00 (MON) | 2017-01-23 11:35:00 (星期一) | 61.583333333333333 61.583333333333333\nB |乙 | 2017-01-18 17:53:00 (WED) | 2017-01-18 17:53:00 (周三) | 2017-01-19 08:00:00 (THU) | 2017-01-19 08:00:00 (星期四) | .116666666666667 .116666666666667\nC | C | 2017-01-13 13:00:00 (FRI) | 2017-01-13 13:00:00 (周五) | 2017-01-17 14:52:00 (TUE) | 2017-01-17 14:52:00 (周二) | 31.866666666666667 31.866666666666667\nD | D | 2017-01-21 10:00:00 (SAT) | 2017-01-21 10:00:00 (周六) | 2017-01-30 08:52:00 (MON) | 2017-01-30 08:52:00 (星期一) | 68.866666666666667 68.866666666666667\n

db<>fiddle here db<> 在这里摆弄


Previous solution:以前的解决方案:

You can use a correlated hierarchical query to generate one row for each work day and then sum the hours for each day:您可以使用相关的分层查询为每个工作日生成一行,然后对每天的小时数求和:

SELECT task,
       COALESCE( SUM( end_time - start_time ), 0 ) * 24 AS total_hours
FROM   (
  SELECT task,
         GREATEST( t.start_time, d.column_value + INTERVAL '8' HOUR ) AS start_time,
         LEAST( t.end_time, d.column_value + INTERVAL '18' HOUR ) AS end_time
  FROM   your_table t
         LEFT OUTER JOIN
         TABLE(
           CAST(
             MULTISET(
               SELECT TRUNC( t.start_time + LEVEL - 1 )
               FROM   DUAL
               WHERE  TRUNC( t.start_time + LEVEL - 1 ) - TRUNC( t.start_time + LEVEL - 1, 'iw' ) < 6
               CONNECT BY TRUNC( t.start_time + LEVEL - 1 ) < t.end_time
             ) AS SYS.ODCIDATELIST
           )
         ) d
         ON (   t.end_time   > d.column_value + INTERVAL  '8' HOUR
            AND t.start_time < d.column_value + INTERVAL '18' HOUR )
)
GROUP BY task;

My favorite of this problem is to use the build-in SCHEDULER SCHEDULE .我最喜欢这个问题是使用内置的SCHEDULER SCHEDULE

You have to create a function using DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING您必须使用DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING创建一个函数

First, create some schedules for exceptions like public holidays, if required.首先,如果需要,为公共假期等例外情况制定一些时间表。 Here an example for US bank days:这是美国银行工作日的示例:

BEGIN
    DBMS_SCHEDULER.CREATE_SCHEDULE('NEW_YEARS_DAY', repeat_interval => 'FREQ=YEARLY;INTERVAL=1;BYDATE=0101');
    DBMS_SCHEDULER.CREATE_SCHEDULE('MARTIN_LUTHER_KING_DAY', repeat_interval => 'FREQ=MONTHLY;BYMONTH=JAN;BYDAY=3 MON', comments => 'Third Monday of January');
    DBMS_SCHEDULER.CREATE_SCHEDULE('WASHINGTONS_BIRTHDAY', repeat_interval => 'FREQ=MONTHLY;BYMONTH=FEB;BYDAY=3 MON', comments => 'Third Monday of February');
    DBMS_SCHEDULER.CREATE_SCHEDULE('MEMORIAL_DAY', repeat_interval => 'FREQ=MONTHLY;BYMONTH=MAY;BYDAY=-1 MON', comments => 'Last Monday of May');
    DBMS_SCHEDULER.CREATE_SCHEDULE('INDEPENDENCE_DAY', repeat_interval => 'FREQ=YEARLY;INTERVAL=1;BYDATE=0704');
    DBMS_SCHEDULER.CREATE_SCHEDULE('CHRISTMAS_DAY', repeat_interval => 'FREQ=YEARLY;INTERVAL=1;BYDATE=1225');
    DBMS_SCHEDULER.CREATE_SCHEDULE('SPRING_BREAK', repeat_interval => 'FREQ=YEARLY;BYDATE=0301+SPAN:7D');
END;

or another example for German bank days:或德国银行工作日的另一个例子:

BEGIN
    DBMS_SCHEDULER.CREATE_SCHEDULE('New_Year', repeat_interval => 'FREQ=YEARLY;BYDATE=0101');

    DBMS_SCHEDULER.CREATE_SCHEDULE('Easter_Sunday',  repeat_interval => 'FREQ=YEARLY;BYDATE=20150405,    20160327,    20170416,    20170416,    20180401,    20190421,    20200412', comments => 'Hard coded till 2020');
    DBMS_SCHEDULER.CREATE_SCHEDULE('Good_Friday',    repeat_interval => 'FREQ=YEARLY;BYDATE=20150405-2D, 20160327-2D, 20170416-2D, 20170416-2D, 20180401-2D, 20190421-2D, 20200412-2D', comments => '2 Days before Easter');
    DBMS_SCHEDULER.CREATE_SCHEDULE('Easter_Monday',   repeat_interval => 'FREQ=YEARLY;BYDATE=20150405+1D, 20160327+1D, 20170416+1D, 20170416+1D, 20180401+1D, 20190421+1D, 20200412+1D', comments => '1 Day after Easter');
    DBMS_SCHEDULER.CREATE_SCHEDULE('Ascension_Day',   repeat_interval => 'FREQ=YEARLY;BYDATE=20150405+39D,20160327+39D,20170416+39D,20170416+39D,20180401+39D,20190421+39D,20200412+39D', comments => '39 Days after Easter');
    DBMS_SCHEDULER.CREATE_SCHEDULE('Pentecost_Monday', repeat_interval => 'FREQ=YEARLY;BYDATE=20150405+50D,20160327+50D,20170416+50D,20170416+50D,20180401+50D,20190421+50D,20200412+50D', comments => '50 Days after easter');

    DBMS_SCHEDULER.CREATE_SCHEDULE('Repentance_and_Prayer', repeat_interval => 'FREQ=DAILY;BYDATE=1122-SPAN:7D;BYDAY=WED', 
        comments => 'Wednesday before November 23th, Buss- und Bettag');
    -- alternative solution: 
    --DBMS_SCHEDULER.CREATE_SCHEDULE('Repentance_and_Prayer', repeat_interval => 'FREQ=MONTHLY;BYMONTH=NOV;BYDAY=3 WED', 
    --    comments => '3rd Wednesday in November');

    DBMS_SCHEDULER.CREATE_SCHEDULE('Labor_Day', repeat_interval => 'FREQ=YEARLY;BYDATE=0501');
    DBMS_SCHEDULER.CREATE_SCHEDULE('German_Unity_Day', repeat_interval => 'FREQ=YEARLY;BYDATE=1003');
    DBMS_SCHEDULER.CREATE_SCHEDULE('Christmas', repeat_interval => 'FREQ=YEARLY;BYDATE=1225+SPAN:2D');

    DBMS_SCHEDULER.CREATE_SCHEDULE('Christian_Celebration_Days', repeat_interval => 'FREQ=DAILY;INTERSECT=Easter_Sunday,Good_Friday,Easter_Monday,Ascension_Day,Pentecost_Monday,Repentance_and_Prayer,Christmas');
    -- alternative solution: 
    -- DBMS_SCHEDULER.CREATE_SCHEDULE('Christian_Celebration_Days', repeat_interval => 'FREQ=Good_Friday;BYDAY=1 MON, 6 THU,8 MON');
    DBMS_SCHEDULER.CREATE_SCHEDULE('Political_Holidays', repeat_interval => 'FREQ=DAILY;INTERSECT=New_Year,Labor_Day,German_Unity_Day');

END;
/

See syntax for calendar here: Calendaring Syntax在此处查看日历的语法日历语法

Then create a function similar to this:然后创建一个类似这样的函数:

CREATE OR REPLACE FUNCTION GetBusinessHours(start_time IN TIMESTAMP, end_time IN TIMESTAMP) RETURN INTERVAL DAY TO SECOND AS
    next_run_date TIMESTAMP := start_time;
     duration INTERVAL DAY(3) TO SECOND(0) := INTERVAL '0' HOUR;
BEGIN
    LOOP
        DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=HOURLY;INTERVAL=1;BYHOUR=8,9,10,11,13,14,15,16,17;BYDAY=MON,TUE,WED,THU,FRI,SAT; EXCLUDE=NEW_YEARS_DAY,MARTIN_LUTHER_KING_DAY,WASHINGTONS_BIRTHDAY,MEMORIAL_DAY,INDEPENDENCE_DAY,CHRISTMAS_DAY,SPRING_BREAK', NULL, next_run_date, next_run_date);
        duration := duration + INTERVAL '1' HOUR;
        EXIT WHEN next_run_date >= end_time;
    END LOOP;
    RETURN duration;
END;

CREATE OR REPLACE FUNCTION GetBusinessStart(start_time IN TIMESTAMP, end_time IN TIMESTAMP) RETURN TIMESTAMP AS
    next_run_date TIMESTAMP := start_time;
BEGIN
    DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=HOURLY;INTERVAL=1;BYHOUR=8,9,10,11,13,14,15,16,17;BYDAY=MON,TUE,WED,THU,FRI,SAT; EXCLUDE=NEW_YEARS_DAY,MARTIN_LUTHER_KING_DAY,WASHINGTONS_BIRTHDAY,MEMORIAL_DAY,INDEPENDENCE_DAY,CHRISTMAS_DAY,SPRING_BREAK', NULL, next_run_date, next_run_date);
    RETURN next_run_date;
END;


CREATE OR REPLACE FUNCTION GetBusinessEnd(start_time IN TIMESTAMP, end_time IN TIMESTAMP) RETURN TIMESTAMP AS
    next_run_date TIMESTAMP := start_time;
BEGIN
    LOOP
        DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=HOURLY;INTERVAL=1;BYHOUR=8,9,10,11,13,14,15,16,17;BYDAY=MON,TUE,WED,THU,FRI,SAT; EXCLUDE=NEW_YEARS_DAY,MARTIN_LUTHER_KING_DAY,WASHINGTONS_BIRTHDAY,MEMORIAL_DAY,INDEPENDENCE_DAY,CHRISTMAS_DAY,SPRING_BREAK', NULL, next_run_date, next_run_date);
        EXIT WHEN next_run_date >= end_time;
    END LOOP;
    RETURN next_run_date;
END;

If you don't have to consider pubic holidays, just skip EXCLUDE=... part.如果您不必考虑公共假期,只需跳过EXCLUDE=...部分。

Then you can use the function in your query:然后您可以在查询中使用该函数:

SELECT TASK, 
   GetBusinessStart(START_TIME, END_TIME),
   GetBusinessEnd(START_TIME, END_TIME),
   GetBusinessHours(START_TIME, END_TIME)
FROM ...;

Note, the function would need some fine-tuning in case START_TIME and END_TIME fall both into the same non-working day.请注意,如果 START_TIME 和 END_TIME 属于同一个非工作日,则该函数需要进行一些微调。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM