Calculate time diffrence in SQL with shifts - sql

i have two table
the first table contains the record of a ticket with start date and end date
start_date | End_Date
21-02-2017 07:52:32 | 22-02-2017 09:56:32
21-02-2017 09:52:32 | 23-02-2017 17:52:32
the second table contains the details of the weekly shift:
shift_day | Start_Time | End_Time
MON 9:00 18:00
TUE 10:00 19:00
WED 9:00 18:00
THU 10:00 19:00
FRI 9:00 18:00
I am looking to get the time difference in the first table which will only include the time as per the second table.

Use a recursive sub-query factoring clause to generate each day within your time ranges and then correlate that with your shifts to restrict the time for each day to be within the shift hours and then aggregate to get the total:
Oracle 18 Setup:
CREATE TABLE times ( start_date, End_Date ) AS
SELECT DATE '2017-02-21' + INTERVAL '07:52:32' HOUR TO SECOND,
DATE '2017-02-22' + INTERVAL '09:56:32' HOUR TO SECOND
FROM DUAL
UNION ALL
SELECT DATE '2017-02-21' + INTERVAL '09:52:32' HOUR TO SECOND,
DATE '2017-02-23' + INTERVAL '17:52:32' HOUR TO SECOND
FROM DUAL;
CREATE TABLE weekly_shifts ( shift_day, Start_Time, End_Time ) AS
SELECT 'MON', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL;
Query 1:
WITH days ( id, start_date, day_start, day_end, end_date ) AS (
SELECT ROWNUM,
start_date,
start_date,
LEAST( TRUNC( start_date ) + INTERVAL '1' DAY, end_date ),
end_date
FROM times
UNION ALL
SELECT id,
start_date,
day_end,
LEAST( day_end + INTERVAL '1' DAY, end_date ),
end_date
FROM days
WHERE day_end < end_date
)
SELECT start_date,
end_date,
SUM( shift_end - shift_start ) AS days_worked_on_shift
FROM (
SELECT ID,
start_date,
end_date,
GREATEST( day_start, TRUNC( day_start ) + start_time ) AS shift_start,
LEAST( day_end, TRUNC( day_start ) + end_time ) AS shift_end
FROM days d
INNER JOIN
weekly_shifts w
ON ( TO_CHAR( d.day_start, 'DY' ) = w.shift_day )
)
GROUP BY id, start_date, end_date;
Result:
START_DATE END_DATE DAYS_WORKED_ON_SHIFT
------------------- ------------------- --------------------
2017-02-21 07:52:32 2017-02-22 09:56:32 0.414259259259259259
2017-02-21 09:52:32 2017-02-23 17:52:32 1.078148148148148148

Related

Split date range in weeks in oracle SQL

I need to split the following data in Oracle SQL:
WITH sample_data AS
(SELECT DATE '2020-12-16' Start_Date, DATE '2021-01-07' End_Date FROM DUAL)
in week ranges for every working week(from Monday to Friday) of this given period. The final result it should look like this:
NEW_STARTDATE NEW_END_DATE
2020-12-16 2020-12-18
2020-12-21 2020-12-25
2020-12-28 2021-01-01
2021-01-04 2021-01-07
So in this example, the first row starts with the initial start date (2020-12-16) which is on Wednesday and continues with the new end date(2020-12-18) which is the next Friday, and so on with ranges of working weeks until the actual end date of this period.
You can use:
WITH sample_data ( start_date, end_date ) AS (
SELECT DATE '2020-12-16', DATE '2021-01-07' FROM DUAL
),
weeks ( start_date, week_start, week_end, end_date ) AS (
SELECT start_date,
TRUNC( start_date, 'IW' ),
LEAST( TRUNC( start_date, 'IW' ) + INTERVAL '4' DAY, end_date ),
end_date
FROM sample_data
UNION ALL
SELECT start_date,
week_start + INTERVAL '7' DAY,
LEAST( week_end + INTERVAL '7' DAY, end_date ),
end_date
FROM weeks
WHERE week_start + INTERVAL '7' DAY <= end_date
)
SELECT GREATEST( week_start, start_date ) AS new_start_date,
week_end AS new_end_date
FROM weeks
WHERE GREATEST( week_start, start_date ) <= week_end;
Which outputs (where the NLS_DATE_FORMAT is YYYY-MM-DD (DY)):
NEW_START_DATE
NEW_END_DATE
2020-12-16 (WED)
2020-12-18 (FRI)
2020-12-21 (MON)
2020-12-25 (FRI)
2020-12-28 (MON)
2021-01-01 (FRI)
2021-01-04 (MON)
2021-01-07 (THU)
db<>fiddle here
Here is one way - compute the Monday dates for the ISO week of the input dates (start_date and end_date), while also keeping track of which is the first and which is the last such Monday in the same hierarchical (connect by) query. Then produce the requested output; for the first week, check that the start_date is not a Saturday or a Sunday (if it is, that "first week" should not produce a row in the output); that is done in the where clause. This is illustrated in the sample dates I used for testing - the input "start date" is a Saturday, so the first "new start date" is the following Monday.
with
sample_data (start_date, end_date) as (
select date '2020-12-12', date '2021-01-02' from dual
)
, mondays (dt, rn, mx) as (
select trunc(start_date, 'iw') + 7 * (level - 1), level, max(level) over ()
from sample_data
connect by level <= 1 + (trunc(end_date, 'iw') - trunc(start_date, 'iw'))/7
)
select case rn when 1 then greatest(start_date, dt)
else dt end as new_start_date,
case rn when mx then least(dt + 4, end_date)
else dt + 4 end as new_end_date
from sample_data cross join mondays
where rn >= 2 or start_date <= dt + 4
;
NEW_START_DATE NEW_END_DATE
--------------- ---------------
MON 14-DEC-2020 FRI 18-DEC-2020
MON 21-DEC-2020 FRI 25-DEC-2020
MON 28-DEC-2020 FRI 01-JAN-2021

Using TIME_DIFF with multiple conditions in Google BigQuery

I am trying to calculate the worked hours for specific days in Google BigQuery (SQL).
The pay wage is $10 when you work on a day time but $15 when you work on a night time.
Day time is defined as 6am to 10pm whereas night time is defined as 10pm to 6am.
Employees can work flexibly as they are limousine drivers.
The following is an example of my table:
id
start_at
end_at
date
abc123
04:00:00
07:00:00
2020-01-05
abc123
09:00:00
15:32:00
2020-01-05
abc123
23:00:00
23:35:00
2020-01-05
abc123
23:40:00
23:59:00
2020-01-05
abc123
23:59:00
01:35:00
2020-01-05
abc123
02:02:00
04:35:00
2020-01-06
abc123
05:40:00
06:59:00
2020-01-06
So the actual work hours is calculated by taking the difference between start_at and end_at but the day time and night time conditions are becoming a hassle in my query..
*the date column is based on start_at. Even when you start at 11:59pm and end at the next day 12:05am, the date follows the date of the start_at instead of end_at.
Any ideas? Thanks in advance!
Consider below solution
create temp function night_day_split(start_at time, end_at time, date date) as (array(
select as struct
extract(date from time_point) day,
if(extract(hour from time_point) between 6 and 22, 'day', 'night') day_night,
count(1) minutes
from unnest(generate_timestamp_array(
timestamp(datetime(date, start_at)),
timestamp(datetime(if(start_at < end_at, date, date + 1), end_at)),
interval 1 minute
)) time_point
group by 1, 2
));
select id, day,
sum(if(day_night = 'day', minutes, null)) day_minutes,
sum(if(day_night = 'night', minutes, null)) night_minutes
from yourtable,
unnest(night_day_split(start_at, end_at, date)) v
group by id, day
if applied to sample data in your question - output is
You can try following code :-
with mytable as (
select 'abc123' id, cast( '04:00:00' as time) start_dt, cast( '07:00:00' as time) end_dt, date('2020-01-05' ) date union all
select 'abc123', cast( '09:00:00' as time), cast( '15:32:00' as time), date('2020-01-05') union all
select 'abc123', cast( '23:00:00' as time), cast( '23:35:00' as time), date('2020-01-05' ) union all
select 'abc123', cast('23:40:00' as time), cast( '23:59:00' as time), date('2020-01-05') union all
select 'abc123', cast ('23:59:00' as time), cast( '01:35:00' as time), date('2020-01-05') union all
select 'abc123', cast('02:02:00' as time), cast( '04:35:00' as time), date('2020-01-06') union all
select 'abc123', cast('05:40:00' as time), cast( '06:59:00' as time), date('2020-01-06')
)
select id, date, sum (value) as sal from(
select id, date,
case when start_dt > cast( '06:00:00' as time) and end_dt < cast( '22:00:00' as time) and start_dt < end_dt then (time_diff(end_dt, start_dt, Minute)/60) * 10
when start_dt < cast( '06:00:00' as time) and end_dt < cast( '06:00:00' as time) then (time_diff(end_dt, start_dt, Minute)/60) * 15
when start_dt < cast( '06:00:00' as time) and end_dt < cast( '22:00:00' as time) then (time_diff(cast( '06:00:00' as time), start_dt, Minute)/60) * 15 + (time_diff( end_dt,cast( '06:00:00' as time), Minute)/60) * 10
when start_dt > cast( '22:00:00' as time) and end_dt < cast( '06:00:00' as time) then (time_diff(cast( '23:59:00' as time), start_dt, Minute)/60) * 15 + (time_diff( end_dt,cast( '00:00:00' as time), Minute)/60) * 15
when start_dt > cast( '22:00:00' as time) and end_dt > cast( '22:00:00' as time) then (time_diff(end_dt, start_dt, Minute)/60) * 15
else 0
end as value
from mytable) group by id, date
Output :-
You can further group by on month for monthly salary.

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

Oracle query Round up or down to nearest 15 minute interval

08-SEP-20 08:55:05
08-SEP-20 15:36:13
The query below is working correctly for 15:36:13 in that it rounds to 15:30 but the 8:55:05 is rounding down to 08:45 when it should be rounding to 09:00
select event_date,trunc(event_date,'mi') - numtodsinterval( mod(to_char(event_date,'mi'),15), 'minute' ) as nearest_quarter
from time_source_in_all where empno = '002307718' and event_date between '8-sep-2020' and '9-sep-2020'
I think this will do what you want:
select trunc(event_date, 'HH') + round(extract(minute from event_date) / 15) * interval '15' minute )
. . .
Note: I prefer the extract() because it is standard SQL. But it assumes that the column is a timestamp and not a date.
or the equivalent:
select trunc(event_date, 'HH') + numtodsinterval(round(to_number(to_char(event_date, 'MI')) / 15) * 15, 'minute')
You can use:
SELECT event_date,
TRUNC( event_date, 'HH' )
+ ROUND( EXTRACT( MINUTE FROM CAST( event_date AS TIMESTAMP ) ) / 15 )
* INTERVAL '15' MINUTE
AS rounded_15_event_date
FROM table_name
or:
SELECT event_date,
TRUNC( event_date, 'HH' )
+ ROUND( ( event_date - TRUNC( event_date, 'HH' ) ) * 24 * 4 )
* INTERVAL '15' MINUTE
AS rounded_15_event_date
FROM table_name
Which, for your sample data:
CREATE TABLE table_name ( event_date ) AS
SELECT DATE '2020-09-08' + INTERVAL '08:55:05' HOUR TO SECOND FROM DUAL UNION ALL
SELECT DATE '2020-09-08' + INTERVAL '15:36:13' HOUR TO SECOND FROM DUAL
Both output:
EVENT_DATE | ROUNDED_15_EVENT_DATE
:------------------ | :--------------------
2020-09-08 08:55:05 | 2020-09-08 09:00:00
2020-09-08 15:36:13 | 2020-09-08 15:30:00
db<>fiddle here

Get day number when all I have is day of the week

I was interested in MT0's answer on this question using intervals and dates. I was working through trying to find a different way to answer the question and I started to wonder about something.
Using just the intervals that MT0 set up:
with weekly_shifts(shift_date,start_time,end_time) as
(SELECT 'MON', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL)
If all I have is days of the week in DY format (MON,TUE,WED) and I want to get the number version of the day (2,3,4), what is the easiest way to do that?
My only idea that I could come up with was something like this:
select to_char(next_day(sysdate,shift_date),'D') SHIFT_NUM,
weekly_shifts.*
from weekly_shifts
You can create a look-up table with all the day names and numbers and then join to that:
CREATE TABLE day_numbers(day_number, day_name) AS
SELECT TO_CHAR(SYSDATE + LEVEL, 'D'),
TO_CHAR(SYSDATE + LEVEL, 'DY')
FROM DUAL
CONNECT BY LEVEL <= 7;
Or, you can generate it on the fly as part of the sub-query factoring clause:
WITH day_numbers(day_number, day_name) AS (
SELECT TO_CHAR(SYSDATE + LEVEL, 'D'),
TO_CHAR(SYSDATE + LEVEL, 'DY')
FROM DUAL
CONNECT BY LEVEL <= 7
),
weekly_shifts(shift_date,start_time,end_time) as (
SELECT 'MON', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL
)
db<>fiddle here
One possibility is using row_number() and rownum. Need to be thoroughly tested as your example probably simplified:
with weekly_shifts
AS
(
SELECT 'MON' shift_day, INTERVAL '09:00' HOUR TO MINUTE start_time, INTERVAL '18:00' HOUR TO MINUTE end_time FROM DUAL
UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'MON' shift_day, INTERVAL '09:00' HOUR TO MINUTE start_time, INTERVAL '18:00' HOUR TO MINUTE end_time FROM DUAL
UNION ALL
SELECT 'TUE', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'WED', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'THU', INTERVAL '10:00' HOUR TO MINUTE, INTERVAL '19:00' HOUR TO MINUTE FROM DUAL
UNION ALL
SELECT 'FRI', INTERVAL '09:00' HOUR TO MINUTE, INTERVAL '18:00' HOUR TO MINUTE FROM DUAL
)
SELECT ROW_NUMBER() OVER (PARTITION BY reset_week ORDER BY reset_week)+1 day_number, shift_day, start_time, end_time, reset_week
FROM
(
SELECT ROW_NUMBER() OVER (PARTITION BY shift_day ORDER BY rownum) reset_week, shift_day, start_time, end_time FROM weekly_shifts
ORDER BY rownum
)
ORDER BY reset_week
/
Output:
day_number shift_day start_time end_time reset_week
2 MON +00 09:00:00.000000 +00 18:00:00.000000 1
3 TUE +00 10:00:00.000000 +00 19:00:00.000000 1
4 WED +00 09:00:00.000000 +00 18:00:00.000000 1
5 THU +00 10:00:00.000000 +00 19:00:00.000000 1
6 FRI +00 09:00:00.000000 +00 18:00:00.000000 1
2 MON +00 09:00:00.000000 +00 18:00:00.000000 2
3 TUE +00 10:00:00.000000 +00 19:00:00.000000 2
4 WED +00 09:00:00.000000 +00 18:00:00.000000 2
5 THU +00 10:00:00.000000 +00 19:00:00.000000 2
6 FRI +00 09:00:00.000000 +00 18:00:00.000000 2