[英]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
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.