find number of hours gap when no one is working - sql

i have a table view of table
taking scenario of a hotel that works 24/7 non stop, i want to calculate total number of hours in a month when no one is present .
in image you can see we have shift hours in 24hh format, shifts are 8 hours long and different employees may have different week offs , we have date range for which this shift would be valid , i.e. a typical month and we have planned leaves when employee is completely off.
can you suggest oracle SQL to find total number of hours when no employee is working in a month.

You can generate the shifts and then find the shifts where no-one worked:
WITH shifts (shift_start) AS (
SELECT DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
FROM DUAL
CONNECT BY
DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
< DATE '2022-08-01'
)
SELECT s.shift_start
FROM shifts s
WHERE NOT EXISTS(
SELECT 1
FROM work_master w
WHERE w.shift_date_frm <= s.shift_start
AND s.shift_start < w.shift_date_to + INTERVAL '1' DAY
AND ':' || w.week_off_day || ':' NOT LIKE '%:' || TO_CHAR(s.shift_start, 'Dy') || ':%'
AND EXTRACT(HOUR FROM CAST(s.shift_start AS TIMESTAMP)) = w.shift_start
AND (
(
w.vac_date_frm IS NULL
AND w.vac_date_to IS NULL
)
OR NOT (
w.vac_date_frm <= s.shift_start
AND s.shift_start < w.vac_date_to + INTERVAL '1' DAY
)
)
)
Which, for the sample data:
CREATE TABLE work_master (
employee_name,
shift_start,
shift_end,
shift_date_frm,
shift_date_to,
vac_date_frm,
vac_date_to,
week_off_day
) AS
SELECT 'emp1', 22, 6, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp2', 14, 22, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp3', 6, 14, DATE '2022-07-01', DATE '2022-07-31', DATE '2022-07-27', DATE '2022-07-27', 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp4', 14, 22, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Fri:Sat' FROM DUAL UNION ALL
SELECT 'emp5', 22, 6, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Wed:Thu' FROM DUAL;
Outputs:
SHIFT_START
2022-07-02 06:00:00 (Sat)
2022-07-02 14:00:00 (Sat)
2022-07-03 06:00:00 (Sun)
2022-07-09 06:00:00 (Sat)
2022-07-09 14:00:00 (Sat)
2022-07-10 06:00:00 (Sun)
2022-07-16 06:00:00 (Sat)
2022-07-16 14:00:00 (Sat)
2022-07-17 06:00:00 (Sun)
2022-07-23 06:00:00 (Sat)
2022-07-23 14:00:00 (Sat)
2022-07-24 06:00:00 (Sun)
2022-07-27 06:00:00 (Wed)
2022-07-30 06:00:00 (Sat)
2022-07-30 14:00:00 (Sat)
2022-07-31 06:00:00 (Sun)
If you just want the total hours then:
WITH shifts (shift_start) AS (
SELECT DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
FROM DUAL
CONNECT BY
DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
< DATE '2022-08-01'
)
SELECT COUNT(*) * 8 AS hours_not_worked
FROM shifts s
WHERE NOT EXISTS(
SELECT 1
FROM work_master w
WHERE w.shift_date_frm <= s.shift_start
AND s.shift_start < w.shift_date_to + INTERVAL '1' DAY
AND ':' || w.week_off_day || ':' NOT LIKE '%:' || TO_CHAR(s.shift_start, 'Dy') || ':%'
AND EXTRACT(HOUR FROM CAST(s.shift_start AS TIMESTAMP)) = w.shift_start
AND (
(
w.vac_date_frm IS NULL
AND w.vac_date_to IS NULL
)
OR NOT (
w.vac_date_frm <= s.shift_start
AND s.shift_start < w.vac_date_to + INTERVAL '1' DAY
)
)
)
Which outputs:
HOURS_NOT_WORKED
128
db<>fiddle here

Related

Split row data base on timestamp SQL Oracle

Good day everyone. I have a table as below. Duration is the time from current state to next state.
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
35
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
How do I split data at 9:00 AM of each day like below:
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
10
10/9/2022 9:00:00 AM
A
25
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
Thank you.
Use a row-generator function to generate extra rows when the timestamp is before 09:00 and the next timestamp is after 09:00 (and calculate the diff value rather than storing it in the table):
SELECT l.ts AS timestamp,
t.state,
ROUND((l.next_ts - l.ts) * 24 * 60, 2) As diff
FROM (
SELECT timestamp,
LEAD(timestamp) OVER (ORDER BY timestamp) AS next_timestamp,
state
FROM table_name
) t
CROSS APPLY (
SELECT GREATEST(
t.timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1
) AS ts,
LEAST(
t.next_timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL
) AS next_ts
FROM DUAL
CONNECT BY
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1 < t.next_timestamp
) l;
Which, for your sample data:
CREATE TABLE table_name (Timestamp, State) AS
SELECT DATE '2022-10-09' + INTERVAL '08:50' HOUR TO MINUTE, 'A' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:25' HOUR TO MINUTE, 'B' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:35' HOUR TO MINUTE, 'C' FROM DUAL UNION ALL
SELECT DATE '2022-10-12' + INTERVAL '09:35' HOUR TO MINUTE, 'D' FROM DUAL;
Outputs:
TIMESTAMP
STATE
DIFF
2022-10-09 08:50:00
A
10
2022-10-09 09:00:00
A
25
2022-10-09 09:25:00
B
10
2022-10-09 09:35:00
C
1405
2022-10-10 09:00:00
C
1440
2022-10-11 09:00:00
C
1440
2022-10-12 09:00:00
C
35
2022-10-12 09:35:00
D
null
fiddle

Oracle modify a recursive CTE to JOIN rows with a table

I have recursive CTE, which is working fine. It's designed to generate a numbers of rows based on the count(*) of the locations table, which in this test CASE is 15 or stop before crossing midnight.
My goal is to populate the schedule table. The schedule_id can be hard coded to 1 for now as I plan to wrap this code in a procedure to pass in values.
First, instead of creating rows for a single date I would prefer to use the function generate_dates_pipelined, which creates rows for a range of dates. For each date add the number of seconds ie 83760, which = a time 23:16:00 to create a start_time.
Second, associating a location_id with a date range row being generated.
Note: a unique location_id must be associated with every row. Secondly, though in my test CASE the location_id are consequently ordered that may not be the case in production. Third, there are only 3 rows instead of 15 because the next row would have crossed midnight.
Thanks in advance for your expertise and to all that answer.
Current output:
START_DATE END_DATE
08192021 23:30:00 08192021 23:35:00
08192021 23:40:00 08192021 23:45:00
08192021 23:50:00 08192021 23:55:00
Desired output:
SCHEDULE_ID LOCATION_ID START_DATE END_DATE
1 1 08192021 23:30:00 08192021 23:35:00
1 2 08192021 23:40:00 08192021 23:45:00
1 3 08192021 23:50:00 08192021 23:55:00
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
create table schedule(
schedule_id NUMBER(4),
location_id number(4),
start_date DATE,
end_date DATE,
CONSTRAINT start_min check (start_date=trunc(start_date,'MI')),
CONSTRAINT end_min check (end_date=trunc(end_date,'MI')),
CONSTRAINT end_gt_start CHECK (end_date >= start_date),
CONSTRAINT same_day CHECK (TRUNC(end_date) = TRUNC(start_date))
);
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 15;
ALTER TABLE locations
ADD ( CONSTRAINT locations_pk
PRIMARY KEY (location_id));
WITH input (start_time) AS (
SELECT TRUNC(SYSDATE) + INTERVAL '23:30' HOUR TO MINUTE FROM DUAL
)
SELECT start_time + (LEVEL-1) * INTERVAL '10' MINUTE
AS start_date,
start_time + (LEVEL-1) * INTERVAL '10' MINUTE + INTERVAL '5' MINUTE
AS end_date
FROM input
CONNECT BY (LEVEL-1) * INTERVAL '10' MINUTE < INTERVAL '1' DAY
AND LEVEL <= (SELECT COUNT(*) FROM locations)
AND start_time + (LEVEL-1) * INTERVAL '10' MINUTE < TRUNC(start_time) + INTERVAL '1' DAY;
One statement to generate the data:
-- The date range
WITH args (start_date, end_date) AS (
SELECT current_date, current_date + 2 FROM dual
)
-- generate entire date range
, dates (adate) AS (
SELECT trunc(start_date) FROM args UNION ALL
SELECT adate + INTERVAL '1' DAY FROM dates, args
WHERE adate < end_date - 1
)
-- generate initial date/times
, init_datetimes (adatetime) AS (
SELECT adate + INTERVAL '83760' SECOND FROM dates
)
-- generate the rest of the times per date until midnight
, init_schedules (adatetime, end_time, n) AS (
SELECT adatetime, adatetime + INTERVAL '5' MINUTE, 1 FROM init_datetimes UNION ALL
SELECT adatetime + INTERVAL '10' MINUTE, adatetime + INTERVAL '10' MINUTE + INTERVAL '5' MINUTE, n+1
FROM init_schedules
WHERE trunc(adatetime) = trunc(adatetime + 2*INTERVAL '10' MINUTE) -- stop before midnight
-- AND n < 10 -- Just for protection
)
-- generate some test locations
, locations (alocation) AS (
SELECT 1 FROM dual UNION
SELECT 3 FROM dual UNION
SELECT 6 FROM dual UNION
SELECT 7 FROM dual UNION
SELECT 9 FROM dual UNION
SELECT 10 FROM dual
)
-- add row_number to location list
, location_list (alocation, n) AS (
SELECT alocation, ROW_NUMBER() OVER (ORDER BY alocation) FROM locations
)
-- Now apply locations to the schedule for each date/time
, location_dates (alocation, adatetime, end_time, n) AS (
SELECT alocation, adatetime, end_time, dat.n
FROM location_list loc
JOIN init_schedules dat
ON loc.n = dat.n -- one location per row per date
)
SELECT *
FROM location_dates
ORDER BY adatetime, alocation
;
The result:
loc date/time end_time n
1 08192021 23:16:00 08192021 23:21:00 1
3 08192021 23:26:00 08192021 23:31:00 2
6 08192021 23:36:00 08192021 23:41:00 3
7 08192021 23:46:00 08192021 23:51:00 4
1 08202021 23:16:00 08202021 23:21:00 1
3 08202021 23:26:00 08202021 23:31:00 2
6 08202021 23:36:00 08202021 23:41:00 3
7 08202021 23:46:00 08202021 23:51:00 4
1 08212021 23:16:00 08212021 23:21:00 1
3 08212021 23:26:00 08212021 23:31:00 2
6 08212021 23:36:00 08212021 23:41:00 3
7 08212021 23:46:00 08212021 23:51:00 4
Nice and compact. I do cross midnight now as another process was fixed that could not handle that situation. Thanks to all who answered.
WITH params AS
(
SELECT 1 AS schedule_id,
TO_DATE ( '2021-08-21 00:00:00'
, 'YYYY-MM-DD HH24:MI:SS'
) AS base_date
, INTERVAL '83760' SECOND AS offset
, INTERVAL '10' MINUTE AS incr
, INTERVAL '5' MINUTE AS duration
FROM dual
)
SELECT p.schedule_id
, l.location_id
, p.base_date
, p.base_date + offset
+ (incr * (ROWNUM - 1)) AS start_date
, p.base_date + offset
+ (incr * (ROWNUM - 1))
+ p.duration AS end_date
FROM locations l
CROSS JOIN params p
ORDER BY start_date
;

Calculating passed service hours

I'm trying to calculate the time that has passed since a service request has been logged (service time), based on service hours.
Start time is the time the ticket has been logged (date_logged), end time would be either the current time (for open tickets) or the date_closed for closed tickets.
Service hours vary depending on the department the ticket was assigned to, as listed in the example below (all the times and dates in my table have the data type datetime):
Ticket-Nr.
Department
date_logged
date_closed
start_mon
end_mon
start_tue
end_tue
start_wed
end_wed
start_trs
end_trs
start_fri
end_fri
start_sat
end_sat
1234567
A
06.01.21 11:30:52
01.01.2001 07:30
01.01.2001 16:45
01.01.2001 07:30
01.01.2001 16:45
01.01.2001 07:30
01.01.2001 16:45
01.01.2001 07:30
01.01.2001 16:45
01.01.2001 07:30
01.01.2001 13:00
8912345
B
13.01.21 09:14:16
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 15:00
6789012
C
14.01.21 10:48:01
14.01.21 11:40
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 16:30
01.01.2001 07:00
01.01.2001 15:00
3456789
D
15.01.2021 09:41:00
01.01.2001 08:00
01.01.2001 15:00
01.01.2001 08:00
01.01.2001 15:00
01.01.2001 08:00
01.01.2001 15:00
01.01.2001 08:00
01.01.2001 15:00
01.01.2001 08:00
01.01.2001 13:00
0123456
B
02.01.2021 13:12:00
...
...
...
...
...
The result should look something like this (minus the "current datetime" column, that's just for context):
Ticket-Nr.
department
date_logged
service time [hh:mm]
current datetime
date_closed
1234567
A
06.01.21 11:30:52
62:33
14.01.2021 12:03
8912345
B
13.01.21 09:14:16
12:19
14.01.2021 12:03
6789012
C
14.01.21 10:48:01
00:28
14.01.2021 12:03
14.01.21 11:40
...
...
...
...
...
...
Public holidays have to be included for department A, B and D.
You can directly calculate the hours (extending your previous question):
SELECT ticket_nr,
department,
date_logged,
current_datetime,
date_closed,
TO_CHAR( FLOOR( service_time_seconds / 60 / 60 ), 'FM99990' )
|| ':'
|| TO_CHAR( MOD( FLOOR( service_time_seconds / 60 ), 60 ), 'FM00' )
|| ':'
|| TO_CHAR( MOD( service_time_seconds, 60 ), 'FM00' )
AS "SERVICE_TIME HH:MM:SS"
FROM (
SELECT t.ticket_nr,
t.department,
t.date_logged,
SYSDATE AS current_datetime,
t.date_closed,
ROUND(
(
-- Calculate the full weeks difference from the start of ISO weeks.
(
TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' )
- TRUNC( date_logged, 'IW' )
) * ( s.hours_mon
+ s.hours_tue
+ s.hours_wed
+ s.hours_thu
+ s.hours_fri
+ s.hours_sat
+ s.hours_sun ) / (7*24)
-- Add the hours for the full days for the final week.
+ DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, 0.0,
1, s.hours_mon,
2, s.hours_mon + s.hours_tue,
3, s.hours_mon + s.hours_tue + s.hours_wed,
4, s.hours_mon + s.hours_tue + s.hours_wed + s.hours_thu,
5, s.hours_mon + s.hours_tue + s.hours_wed + s.hours_thu + s.hours_fri,
6, s.hours_mon + s.hours_tue + s.hours_wed + s.hours_thu + s.hours_fri + s.hours_sat
) / 24
-- Subtract the hours for the full days from the days of the week
-- before the date logged.
- DECODE(
TRUNC( date_logged ) - TRUNC( date_logged, 'IW' ),
0, 0.0,
1, s.hours_mon,
2, s.hours_mon + s.hours_tue,
3, s.hours_mon + s.hours_tue + s.hours_wed,
4, s.hours_mon + s.hours_tue + s.hours_wed + s.hours_thu,
5, s.hours_mon + s.hours_tue + s.hours_wed + s.hours_thu + s.hours_fri,
6, s.hours_mon + s.hours_tue + s.hours_wed + s.hours_thu + s.hours_fri + s.hours_sat
) / 24
-- Add the hours of the final day
+ COALESCE(
GREATEST(
LEAST(
COALESCE( date_closed, SYSDATE ),
TRUNC( COALESCE( date_closed, SYSDATE ) )
+ DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, s.end_mon,
1, s.end_tue,
2, s.end_wed,
3, s.end_thu,
4, s.end_fri,
5, s.end_sat,
6, s.end_sun
)
)
-
(
TRUNC( COALESCE( date_closed, SYSDATE ) )
+ DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, s.start_mon,
1, s.start_tue,
2, s.start_wed,
3, s.start_thu,
4, s.start_fri,
5, s.start_sat,
6, s.start_sun
)
),
0
) / 24,
0
)
-- Subtract the hours of the day before the range starts.
+ COALESCE(
GREATEST(
LEAST(
date_logged,
date_logged
+ DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, s.end_mon,
1, s.end_tue,
2, s.end_wed,
3, s.end_thu,
4, s.end_fri,
5, s.end_sat,
6, s.end_sun
)
)
-
(
date_logged
+ DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, s.start_mon,
1, s.start_tue,
2, s.start_wed,
3, s.start_thu,
4, s.start_fri,
5, s.start_sat,
6, s.start_sun
)
),
0
) / 24,
0
)
)
-- Multiply to give seconds rather than fractions of full days.
* 24 * 60 * 60
) AS service_time_seconds
FROM table_name t
INNER JOIN service_hours s
ON ( s.department = t.department )
);
Which, for the sample data:
CREATE TABLE table_name ( Ticket_Nr, department, date_logged, date_closed ) AS
SELECT 1234567, 'A', DATE '2021-01-06' + INTERVAL '11:30:52' HOUR TO SECOND, NULL FROM DUAL UNION ALL
SELECT 8912345, 'B', DATE '2021-01-13' + INTERVAL '09:14:16' HOUR TO SECOND, NULL FROM DUAL UNION ALL
SELECT 6789012, 'C', DATE '2021-01-14' + INTERVAL '10:48:28' HOUR TO SECOND, DATE '2021-01-21' + INTERVAL '11:40:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 1, 'D', DATE '2021-01-07' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-14' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 2, 'A', DATE '2021-01-07' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-08' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 3, 'A', DATE '2021-01-08' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-09' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 4, 'A', DATE '2021-01-09' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-10' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 5, 'B', DATE '2021-01-09' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-10' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL;
CREATE TABLE service_hours (
Department VARCHAR2(5)
CONSTRAINT service_hours__department__pk PRIMARY KEY,
start_mon INTERVAL DAY TO SECOND,
end_mon INTERVAL DAY TO SECOND,
hours_mon NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_mon ) - ( DATE '1970-01-01' + start_mon ),
0
) * 24
),
start_tue INTERVAL DAY TO SECOND,
end_tue INTERVAL DAY TO SECOND,
hours_tue NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_tue ) - ( DATE '1970-01-01' + start_tue ),
0
) * 24
),
start_wed INTERVAL DAY TO SECOND,
end_wed INTERVAL DAY TO SECOND,
hours_wed NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_wed ) - ( DATE '1970-01-01' + start_wed ),
0
) * 24
),
start_thu INTERVAL DAY TO SECOND,
end_thu INTERVAL DAY TO SECOND,
hours_thu NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_thu ) - ( DATE '1970-01-01' + start_thu ),
0
) * 24
),
start_fri INTERVAL DAY TO SECOND,
end_fri INTERVAL DAY TO SECOND,
hours_fri NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_fri ) - ( DATE '1970-01-01' + start_fri ),
0
) * 24
),
start_sat INTERVAL DAY TO SECOND,
end_sat INTERVAL DAY TO SECOND,
hours_sat NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_sat ) - ( DATE '1970-01-01' + start_sat ),
0
) * 24
),
start_sun INTERVAL DAY TO SECOND,
end_sun INTERVAL DAY TO SECOND,
hours_sun NUMBER
GENERATED ALWAYS AS (
COALESCE(
( DATE '1970-01-01' + end_sun ) - ( DATE '1970-01-01' + start_sun ),
0
) * 24
)
);
INSERT INTO service_hours (
Department,
start_mon, end_mon,
start_tue, end_tue,
start_wed, end_wed,
start_thu, end_thu,
start_fri, end_fri,
start_sat, end_sat,
start_sun, end_sun
)
SELECT 'A',
INTERVAL '07:30' HOUR TO MINUTE, INTERVAL '16:45' HOUR TO MINUTE,
INTERVAL '07:30' HOUR TO MINUTE, INTERVAL '16:45' HOUR TO MINUTE,
INTERVAL '07:30' HOUR TO MINUTE, INTERVAL '16:45' HOUR TO MINUTE,
INTERVAL '07:30' HOUR TO MINUTE, INTERVAL '16:45' HOUR TO MINUTE,
INTERVAL '07:30' HOUR TO MINUTE, INTERVAL '13:00' HOUR TO MINUTE,
NULL, NULL,
NULL, NULL
FROM DUAL UNION ALL
SELECT 'B',
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
NULL, NULL
FROM DUAL UNION ALL
SELECT 'C',
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '16:30' HOUR TO MINUTE,
INTERVAL '07:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
NULL, NULL
FROM DUAL UNION ALL
SELECT 'D',
INTERVAL '08:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
INTERVAL '08:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
INTERVAL '08:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
INTERVAL '08:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
INTERVAL '08:00' HOUR TO MINUTE, INTERVAL '15:00' HOUR TO MINUTE,
NULL, NULL,
NULL, NULL
FROM DUAL;
Outputs:
TICKET_NR | DEPARTMENT | DATE_LOGGED | CURRENT_DATETIME | DATE_CLOSED | SERVICE_TIME HH:MM:SS
--------: | :--------- | :------------------------ | :------------------------ | :------------------------ | :--------------------
1234567 | A | 2021-01-06 11:30:52 (WED) | 2021-01-15 13:38:49 (FRI) | null | 61:13:45
8912345 | B | 2021-01-13 09:14:16 (WED) | 2021-01-15 13:38:49 (FRI) | null | 19:16:37
6789012 | C | 2021-01-14 10:48:28 (THU) | 2021-01-15 13:38:49 (FRI) | 2021-01-21 11:40:00 (THU) | 55:41:40
1 | D | 2021-01-07 07:00:00 (THU) | 2021-01-15 13:38:49 (FRI) | 2021-01-14 07:00:00 (THU) | 35:00:00
2 | A | 2021-01-07 07:00:00 (THU) | 2021-01-15 13:38:49 (FRI) | 2021-01-08 07:00:00 (FRI) | 9:15:00
3 | A | 2021-01-08 07:00:00 (FRI) | 2021-01-15 13:38:49 (FRI) | 2021-01-09 07:00:00 (SAT) | 5:30:00
4 | A | 2021-01-09 07:00:00 (SAT) | 2021-01-15 13:38:49 (FRI) | 2021-01-10 07:00:00 (SUN) | 0:00:00
5 | B | 2021-01-09 07:00:00 (SAT) | 2021-01-15 13:38:49 (FRI) | 2021-01-10 07:00:00 (SUN) | 8:00:00
db<>fiddle here

Oracle query to Exclude weekends, and 6PM to 9PM

I am trying to achieve a query that returns the time difference between two dates excluding weekends(Saturday and Sunday) and excluding time (6 pm-9 am).
For now, I have a function that is excluding the weekends, But I am unable to exclude time from the query. Can anyone help with this?
The article from which I take help is this
CREATE OR REPLACE FUNCTION get_bus_minutes_between(
p_start_date DATE,
p_end_date DATE
)
RETURN NUMBER
DETERMINISTIC -- ***** Can't hurt
IS
days_diff NUMBER := 0;
end_date DATE := p_end_date;
minutes_diff NUMBER;
start_date DATE := p_start_date;
weeks_diff NUMBER;
BEGIN
IF start_date <= end_date
THEN
-- Move start_date and end_date away from weekends
IF start_date > TRUNC (start_date, 'IW') + 5
THEN -- Use next Monday for start_date
start_date := TRUNC (start_date, 'IW') + 7;
END IF;
IF end_date > TRUNC (end_date, 'IW') + 5
THEN -- Use Friday quitting time
end_date := TRUNC (end_date, 'IW') + 4 + (16.5 / 24);
END IF;
-- Move start_date into the same weeek as end_date
-- (Remember how many weeks we had to move it)
weeks_diff := ( TRUNC (end_date, 'IW')
- TRUNC (start_date, 'IW')
) / 7;
IF weeks_diff > 0
THEN
start_date := start_date + (7 * weeks_diff);
END IF;
-- Make start_date the same day as end_date
-- (Remember how many days we had to move it)
days_diff := TRUNC (end_date) - TRUNC (start_date);
IF days_diff > 0
THEN
start_date := start_date + days_diff;
END IF;
-- Move start_date up to starting time
start_date := GREATEST ( start_date
, TRUNC (start_date) + (8.75 / 24)
);
-- Move end_date back to quitting time
end_date := LEAST ( end_date
, TRUNC (end_date) + ( CASE
WHEN TO_CHAR ( end_date
, 'DY'
, 'NLS_DATE_LANGUAGE=ENGLISH'
) = 'FRI'
THEN 16.5
ELSE 17
END
/ 24
)
);
minutes_diff := ( GREATEST ( 0
, end_date - start_date
)
* 24 * 60
)
+ (days_diff * 495) -- 495 minutes per full day (Mon.-Thu.)
+ (weeks_diff * 2445); -- 2445 minutes per full week
ELSIF start_date > end_date
THEN
minutes_diff := -get_bus_minutes_between (end_date, start_date);
ELSE -- One of the arguments was NULL
minutes_diff := NULL;
END IF;
RETURN ROUND(minutes_diff);
END get_bus_minutes_between;
You can directly calculate the difference in days (adapted from my answer here):
SELECT start_date,
end_date,
ROUND(
(
-- Calculate the full weeks difference from the start of ISO weeks.
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * (9/24) * (5/7)
-- Add the full days for the final week.
+ LEAST( TRUNC( end_date ) - TRUNC( end_date, 'IW' ), 5 ) * (9/24)
-- Subtract the full days from the days of the week before the start date.
- LEAST( TRUNC( start_date ) - TRUNC( start_date, 'IW' ), 5 ) * (9/24)
-- Add the hours of the final day
+ LEAST( GREATEST( end_date - TRUNC( end_date ) - 9/24, 0 ), 9/24 )
-- Subtract the hours of the day before the range starts.
- LEAST( GREATEST( start_date - TRUNC( start_date ) - 9/24, 0 ), 9/24 )
)
-- Multiply to give minutes rather than fractions of full days.
* 24 * 60
) AS work_day_mins_diff
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name ( start_date, end_date ) AS
SELECT DATE '2020-12-30' + INTERVAL '00' HOUR, DATE '2020-12-30' + INTERVAL '12' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-30' + INTERVAL '18' HOUR, DATE '2020-12-30' + INTERVAL '20' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-30' + INTERVAL '17:30' HOUR TO MINUTE, DATE '2020-12-30' + INTERVAL '21:30' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT DATE '2021-01-01' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2021-01-02' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-28' + INTERVAL '00' HOUR, DATE '2021-01-04' + INTERVAL '00' HOUR FROM DUAL UNION ALL
SELECT DATE '2020-12-28' + INTERVAL '00' HOUR, DATE '2020-12-29' + INTERVAL '00' HOUR FROM DUAL;
Outputs:
(Using ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS (DY)';)
START_DATE | END_DATE | WORK_DAY_MINS_DIFF
:------------------------ | :------------------------ | -----------------:
2020-12-30 00:00:00 (WED) | 2020-12-30 12:00:00 (WED) | 180
2020-12-30 18:00:00 (WED) | 2020-12-30 20:00:00 (WED) | 0
2020-12-30 17:30:00 (WED) | 2020-12-30 21:30:00 (WED) | 30
2021-01-01 00:00:00 (FRI) | 2021-01-04 00:00:00 (MON) | 540
2021-01-02 00:00:00 (SAT) | 2021-01-04 00:00:00 (MON) | 0
2020-12-28 00:00:00 (MON) | 2021-01-04 00:00:00 (MON) | 2700
2020-12-28 00:00:00 (MON) | 2020-12-29 00:00:00 (TUE) | 540
db<>fiddle here

Calculating passed service hours for open tickets (Oracle SQL)

I'm trying to calculate the time that has passed since a service request has been logged (service time), based on service hours.
Start time is the time the ticket has been logged (date_logged), end time would be either the current time for open tickets or the date_closed for closed tickets.
Service hours are:
Monday to Thursday 07:00 - 16:30
Friday 07:00 - 13:00
So it should look something like this (minus the "current datetime" column, that's just for context):
Ticket-Nr.
date_logged
service time [hh:mm]
current datetime
date_closed
1234567
06.01.21 11:30:52
62:33
14.01.2021 12:03
8912345
13.01.21 09:14:16
12:19
14.01.2021 12:03
6789012
14.01.21 10:48:01
00:28
14.01.2021 12:03
14.01.21 11:40
...
...
...
...
...
I can't say yet whether public holidays have to be included or not, so we can ignore those for now.
I'm thankful for any help whatsoever!
You can calculate the amount of time (adapted from my answers here and here):
SELECT ticket_nr,
date_logged,
current_datetime,
date_closed,
TO_CHAR( FLOOR( service_time_seconds / 60 / 60 ), 'FM9990' )
|| ':'
|| TO_CHAR( MOD( FLOOR( service_time_seconds / 60 ), 60 ), 'FM00' )
|| ':'
|| TO_CHAR( MOD( service_time_seconds, 60 ), 'FM00' )
AS "SERVICE_TIME HH:MM:SS"
FROM (
SELECT ticket_nr,
date_logged,
SYSDATE AS current_datetime,
date_closed,
ROUND(
(
-- Calculate the full weeks difference from the start of ISO weeks.
(
TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' )
- TRUNC( date_logged, 'IW' )
) * (9.5*4+6)/(7*24)
-- Add the hours for the full days for the final week.
+ DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, 0.0,
1, 9.5,
2, 19.0,
3, 28.5,
4, 38.0,
44.0
) / 24
-- Subtract the hours for the full days from the days of the week
-- before the date logged.
- DECODE(
TRUNC( date_logged )
- TRUNC( date_logged, 'IW' ),
0, 0.0,
1, 9.5,
2, 19.0,
3, 28.5,
4, 38.0,
44.0
) / 24
-- Add the hours of the final day
+ LEAST(
GREATEST(
COALESCE( date_closed, SYSDATE )
- ( TRUNC( COALESCE( date_closed, SYSDATE ) )
+ INTERVAL '07:00' HOUR TO MINUTE
),
0
),
DECODE(
TRUNC( COALESCE( date_closed, SYSDATE ) )
- TRUNC( COALESCE( date_closed, SYSDATE ), 'IW' ),
0, 9.5,
1, 9.5,
2, 9.5,
3, 9.5,
4, 6.0,
0.0
) / 24
)
-- Subtract the hours of the day before the range starts.
- LEAST(
GREATEST(
date_logged
- ( TRUNC( date_logged ) + INTERVAL '07:00' HOUR TO MINUTE ),
0
),
DECODE(
TRUNC( date_logged )
- TRUNC( date_logged, 'IW' ),
0, 9.5,
1, 9.5,
2, 9.5,
3, 9.5,
4, 6.0,
0.0
) / 24
)
)
-- Multiply to give seconds rather than fractions of full days.
* 24 * 60 * 60
) AS service_time_seconds
FROM table_name
);
Which, for the sample data:
CREATE TABLE table_name ( Ticket_Nr, date_logged, date_closed ) AS
SELECT 1234567, DATE '2021-01-06' + INTERVAL '11:30:52' HOUR TO SECOND, NULL FROM DUAL UNION ALL
SELECT 8912345, DATE '2021-01-13' + INTERVAL '09:14:16' HOUR TO SECOND, NULL FROM DUAL UNION ALL
SELECT 6789012, DATE '2021-01-14' + INTERVAL '10:48:28' HOUR TO SECOND, DATE '2021-01-21' + INTERVAL '11:40:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 1, DATE '2021-01-07' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-14' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 2, DATE '2021-01-07' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-08' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 3, DATE '2021-01-08' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-09' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT 4, DATE '2021-01-09' + INTERVAL '07:00:00' HOUR TO SECOND, DATE '2021-01-10' + INTERVAL '07:00:00' HOUR TO SECOND FROM DUAL
Outputs (where NLS_DATE_FORMAT is YYYY-MM-DD HH24:MI:SS (DY)):
TICKET_NR | DATE_LOGGED | CURRENT_DATETIME | DATE_CLOSED | SERVICE_TIME HH:MM:SS
--------: | :------------------------ | :------------------------ | :------------------------ | :--------------------
1234567 | 2021-01-06 11:30:52 (WED) | 2021-01-14 12:36:54 (THU) | null | 54:36:02
8912345 | 2021-01-13 09:14:16 (WED) | 2021-01-14 12:36:54 (THU) | null | 12:52:38
6789012 | 2021-01-14 10:48:28 (THU) | 2021-01-14 12:36:54 (THU) | 2021-01-21 11:40:00 (THU) | 44:51:32
1 | 2021-01-07 07:00:00 (THU) | 2021-01-14 12:36:54 (THU) | 2021-01-14 07:00:00 (THU) | 44:00:00
2 | 2021-01-07 07:00:00 (THU) | 2021-01-14 12:36:54 (THU) | 2021-01-08 07:00:00 (FRI) | 9:30:00
3 | 2021-01-08 07:00:00 (FRI) | 2021-01-14 12:36:54 (THU) | 2021-01-09 07:00:00 (SAT) | 6:00:00
4 | 2021-01-09 07:00:00 (SAT) | 2021-01-14 12:36:54 (THU) | 2021-01-10 07:00:00 (SUN) | 0:00:00
db<>fiddle here