patternsqlMinor
MySQL calculate overlapping period excluding overlapping rules
Viewed 0 times
periodexcludingmysqloverlappingcalculaterules
Problem
I would like to calculate the period overlap in hours based on special rules.
I created a test setup, that works as long as the rules themselves don't overlap.
Example non overlapping rules:
When the rules do overlap I need to solve this in our programming language, but I was looking for a database only solution.
Example overlapping rules:
In the below example the users Kevin and Michael don't have overlapping rules and the result is as expected. The user Derrick is affected by rule (id) 1, 2 and 3 (overlapping rules)
Example query:
Output:
I created a test setup, that works as long as the rules themselves don't overlap.
Example non overlapping rules:
id ort_name ort_type time_start time_end factor
------ ---------------- -------- ---------- -------- --------
1 Overtime evening daily 18:00:00 23:59:59 1.20
2 Overtime Night daily 00:00:00 05:00:00 1.20When the rules do overlap I need to solve this in our programming language, but I was looking for a database only solution.
Example overlapping rules:
id ort_name ort_type time_start time_end factor
------ -------------------- -------- ---------- -------- --------
1 Overtime evening daily 18:00:00 23:59:59 1.20
2 Overtime Night daily 00:00:00 05:00:00 1.20
3 Overtime Night Extra daily 01:00:00 03:00:00 1.30In the below example the users Kevin and Michael don't have overlapping rules and the result is as expected. The user Derrick is affected by rule (id) 1, 2 and 3 (overlapping rules)
Example query:
SELECT
OC.ort_rule_id
, ORT.factor
, S.user_name
, S.shift_id
, S.date_start AS shift_start
, S.date_end AS shift_end
, ROUND((TIMESTAMPDIFF(SECOND, S.date_start, S.date_end) / 3600), 2) AS shift_duration_hours
, GREATEST(OC.ort_date_start, S.date_start) AS overlap_start
, LEAST(OC.ort_date_end, S.date_end) AS overlap_end
, ROUND((TIMESTAMPDIFF(SECOND, GREATEST(OC.ort_date_start, S.date_start), LEAST(OC.ort_date_end, S.date_end)) / 3600), 2) AS overlap_duration_hours
FROM
shifts AS S
INNER JOIN ort_calendar AS OC
ON S.date_end > OC.ort_date_start
AND S.date_start = '2014-12-01 00:00:00'
AND
S.date_end < '2014-12-01 00:00:00' + INTERVAL 1 MONTH
ORDER BY
S.user_name ASC, S.date_start ASC, overlap_start ASC, ORT.factor DESCOutput:
Solution
We'll start with a few assumptions/observations/limits ...
Data setups:
Build a new list of
A key part of this solution is next ... breaking down
And now the main event:
```
/
for overlap related values we need to concatentate our
separate dates/times back into datetime values
*/
select or1.id as ort_rule_id,
or1.factor,
dt.user_name,
dt.shift_id,
dt.orig_start as shift_start,
dt.orig_end as shift_end,
round( (timestampdiff(second, dt.orig_start, dt.orig_end) / 3600), 2) as shift_duration_hours,
greatest(cast(concat(dt.date_start,' ',dt.r_time_start) as datetime), dt.orig_start) as overlap_start,
least( cast(concat(dt.date_end ,' ',dt.r_time_end) as datetime), dt.orig_end) as overlap_end,
round( (timestampdiff(second, greatest(cast(concat(dt.date_start,' ',dt.r_time_start) as datetime), dt.orig_start),
least( cast(concat(dt.date_end ,' ',dt.r_time_end) as datetime), dt.orig_end)
) / 3600), 2
) as overlap_duration_hours
from
(
select distinct
/ shift data /
ort_rulestime periods do not span midnight (at least for ort_type = 'daily')
ort_calendartime periods do not span midnight
shiftstime periods can span midnight but at most once (ie, person isn't working from day1 to day2 to day3)
- completely ignoring the
ort_rule.ort_type = {daily | yearly }as we've only got sample 'daily' data to work with
- 'daily' rules apply for all 7 days of the week (ie, weekends are treated just like weekdays)
- don't know what version of MySQL is in use so will skip trying to do this with CTEs (not available prior to MySQL 8.0) and instead use some temp tables
- conducted my testing on 2 different fiddles, with 2 different versions of MySQL; one fiddle allows
create temp table/select/union/selectwhile the other doesn't allow theunionso had to usecreate temp table/insert-select
- while a lot of this code could be included in the main query (as derived tables and/or CTEs), I've opted to use temp tables in order to allow for easier debugging
Data setups:
- copied the provided DDL, moved the create/drop commands around some to get around the 'you cannot drop a table with active FK constraints')
Build a new list of
shifts:/******************************
for shifts that span midnight, break into 2 separate shifts:
1) date_start up to 23:59:59
2) 'tomorrow' from 00:00:00 to date_end
split datetimes into separate date/time parts to make time comparisons a little easier later on
drag along the original shift_id, user_name, date_start/date_end (as dt_start/dt_end) so we don't
have to join back to shifts later
*/
drop table if exists t_shifts;
create temporary table if not exists t_shifts
(shift_id int(10) unsigned not NULL
,user_name varchar(20) default NULL
,orig_start datetime not NULL
,orig_end datetime not NULL
,date_start date not NULL
,time_start time not NULL
,date_end date not NULL
,time_end time not NULL
);
insert into t_shifts (shift_id, user_name, orig_start, orig_end, date_start, time_start, date_end, time_end)
/* shifts that don't span midnight */
select shift_id,
user_name,
date_start,
date_end,
date(date_start),
time(date_start),
date(date_end),
time(date_end)
from shifts
where date(date_start) = date(date_end)
and date_start >= '2014-12-01 00:00:00'
and date_end = '2014-12-01 00:00:00'
and date_end = '2014-12-01 00:00:00'
and date_end <= '2014-12-01 00:00:00' + interval 1 month;A key part of this solution is next ... breaking down
ort_rule time ranges into non-overlapping time ranges.drop table if exists t_time_ranges;
create temporary table if not exists t_time_ranges
(time_start time
,time_end time
);
insert into t_time_ranges (time_start, time_end)
select r1.time_start,
r1.time_end
from ort_rule r1
where r1.time_start != r1.time_end
and not exists(select 1
from ort_rule r2
where r2.id != r1.id
and ( r2.time_start between r1.time_start and r1.time_end
or r2.time_end between r1.time_start and r1.time_end
))
union
select r1.time_start,
r2.time_start
from ort_rule r1
join ort_rule r2
on r2.time_start between r1.time_start and r1.time_end
and r2.id != r1.id
union
select r1.time_start,
r2.time_end
from ort_rule r1
join ort_rule r2
on r2.time_end between r1.time_start and r1.time_end
and r2.time_start < r1.time_start
and r2.id != r2.id
union
select r2.time_end,
r1.time_end
from ort_rule r1
join ort_rule r2
on r2.time_end between r1.time_start and r1.time_end
and r2.id != r1.id;And now the main event:
```
/
for overlap related values we need to concatentate our
separate dates/times back into datetime values
*/
select or1.id as ort_rule_id,
or1.factor,
dt.user_name,
dt.shift_id,
dt.orig_start as shift_start,
dt.orig_end as shift_end,
round( (timestampdiff(second, dt.orig_start, dt.orig_end) / 3600), 2) as shift_duration_hours,
greatest(cast(concat(dt.date_start,' ',dt.r_time_start) as datetime), dt.orig_start) as overlap_start,
least( cast(concat(dt.date_end ,' ',dt.r_time_end) as datetime), dt.orig_end) as overlap_end,
round( (timestampdiff(second, greatest(cast(concat(dt.date_start,' ',dt.r_time_start) as datetime), dt.orig_start),
least( cast(concat(dt.date_end ,' ',dt.r_time_end) as datetime), dt.orig_end)
) / 3600), 2
) as overlap_duration_hours
from
(
select distinct
/ shift data /
Code Snippets
/******************************
for shifts that span midnight, break into 2 separate shifts:
1) date_start up to 23:59:59
2) 'tomorrow' from 00:00:00 to date_end
split datetimes into separate date/time parts to make time comparisons a little easier later on
drag along the original shift_id, user_name, date_start/date_end (as dt_start/dt_end) so we don't
have to join back to shifts later
*/
drop table if exists t_shifts;
create temporary table if not exists t_shifts
(shift_id int(10) unsigned not NULL
,user_name varchar(20) default NULL
,orig_start datetime not NULL
,orig_end datetime not NULL
,date_start date not NULL
,time_start time not NULL
,date_end date not NULL
,time_end time not NULL
);
insert into t_shifts (shift_id, user_name, orig_start, orig_end, date_start, time_start, date_end, time_end)
/* shifts that don't span midnight */
select shift_id,
user_name,
date_start,
date_end,
date(date_start),
time(date_start),
date(date_end),
time(date_end)
from shifts
where date(date_start) = date(date_end)
and date_start >= '2014-12-01 00:00:00'
and date_end <= '2014-12-01 00:00:00' + interval 1 month
union
/* shifts that span midnight - create new shift for date_start - 23:59:59 */
select shift_id,
user_name,
date_start,
date_end,
date(date_start),
time(date_start),
date(date_start),
cast('23:59:59' as time)
from shifts
where date(date_start) != date(date_end)
and date_start >= '2014-12-01 00:00:00'
and date_end <= '2014-12-01 00:00:00' + interval 1 month
union
/* shifts that span midnight - create new shift for 'tomorrow' from 00:00:00 to date_end */
select shift_id,
user_name,
date_start,
date_end,
date(date_end),
cast('00:00:00' as time),
date(date_end),
time(date_end)
from shifts
where date(date_start) != date(date_end)
and date_start >= '2014-12-01 00:00:00'
and date_end <= '2014-12-01 00:00:00' + interval 1 month;drop table if exists t_time_ranges;
create temporary table if not exists t_time_ranges
(time_start time
,time_end time
);
insert into t_time_ranges (time_start, time_end)
select r1.time_start,
r1.time_end
from ort_rule r1
where r1.time_start != r1.time_end
and not exists(select 1
from ort_rule r2
where r2.id != r1.id
and ( r2.time_start between r1.time_start and r1.time_end
or r2.time_end between r1.time_start and r1.time_end
))
union
select r1.time_start,
r2.time_start
from ort_rule r1
join ort_rule r2
on r2.time_start between r1.time_start and r1.time_end
and r2.id != r1.id
union
select r1.time_start,
r2.time_end
from ort_rule r1
join ort_rule r2
on r2.time_end between r1.time_start and r1.time_end
and r2.time_start < r1.time_start
and r2.id != r2.id
union
select r2.time_end,
r1.time_end
from ort_rule r1
join ort_rule r2
on r2.time_end between r1.time_start and r1.time_end
and r2.id != r1.id;/******************************
for overlap related values we need to concatentate our
separate dates/times back into datetime values
*/
select or1.id as ort_rule_id,
or1.factor,
dt.user_name,
dt.shift_id,
dt.orig_start as shift_start,
dt.orig_end as shift_end,
round( (timestampdiff(second, dt.orig_start, dt.orig_end) / 3600), 2) as shift_duration_hours,
greatest(cast(concat(dt.date_start,' ',dt.r_time_start) as datetime), dt.orig_start) as overlap_start,
least( cast(concat(dt.date_end ,' ',dt.r_time_end) as datetime), dt.orig_end) as overlap_end,
round( (timestampdiff(second, greatest(cast(concat(dt.date_start,' ',dt.r_time_start) as datetime), dt.orig_start),
least( cast(concat(dt.date_end ,' ',dt.r_time_end) as datetime), dt.orig_end)
) / 3600), 2
) as overlap_duration_hours
from
(
select distinct
/* shift data */
ts.shift_id,
ts.user_name,
ts.orig_start,
ts.orig_end,
ts.date_start,
ts.time_start,
ts.date_end,
ts.time_end,
/* rule/time range data */
ttr.time_start as r_time_start,
ttr.time_end as r_time_end
from t_shifts ts
left
join t_time_ranges ttr
on ttr.time_start between ts.time_start and ts.time_end
or ttr.time_end between ts.time_start and ts.time_end
) dt
join ort_calendar oc
on dt.orig_start < oc.ort_date_end
and dt.orig_end > oc.ort_date_start
join ort_rule or1
on or1.id = oc.ort_rule_id
and dt.r_time_start >= or1.time_start
and dt.r_time_end <= or1.time_end
and or1.factor = (select max(or2.factor)
from ort_rule or2
where dt.r_time_start >= or2.time_start
and dt.r_time_end <= or2.time_end)
order by dt.user_name asc,
shift_start asc,
overlap_start asc,
or1.factor desc;
ort_rule_id factor user_name shift_id shift_start shift_end shift_duration_hours overlap_start overlap_end overlap_duration_hours
----------- ------ --------- -------- ------------------- ------------------- -------------------- ------------------- ------------------- ----------------------
1 1.2 Derrick 8 2014-12-09 15:00:00 2014-12-10 06:12:27 15.21 2014-12-09 18:00:00 2014-12-09 23:59:59 6.00
2 1.2 Derrick 8 2014-12-09 15:00:00 2014-12-10 06:12:27 15.21 2014-12-10 00:00:00 2014-12-10 01:00:00 1.00
3 1.3 Derrick 8 2014-12-09 15:00:00 2014-12-10 06:12:27 15.21 2014-12-10 01:00:00 2014-12-10 03:00:00 2.00
2 1.2 Derrick 8 2014-12-09 15:00:00 2014-12-10 06:12:27 15.21 2014-12-10 03:00:00 20Context
StackExchange Database Administrators Q#85929, answer score: 2
Revisions (0)
No revisions yet.