26

I have a MySQL table with the following fields:

  • name
  • starttime
  • endtime

starttime and endtime are MySQL TIME fields (not DATETIME). I need a way to periodically "scan" the table to see if there are any overlaps in time ranges within the table. If there is an event from 10:00-11:00 and another from 10:30-11:30, I want to be alerted of the presence of the time overlap.

Nothing fancy really, all I want to know whether an overlap exists or not.

I'm going to be using PHP to execute this.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
Danish M.
  • 997
  • 1
  • 10
  • 19

5 Answers5

42

This is a query pattern for which I found the answer many years ago:

SELECT *
FROM mytable a
JOIN mytable b on a.starttime <= b.endtime
    and a.endtime >= b.starttime
    and a.name != b.name; -- ideally, this would compare a "key" column, eg id

To find "any overlap", you compare the opposite ends of the timeframe with each other. It's something I had to get a pen and paper out for and draw adjacent ranges to realise that the edge cases boiled down to this comparison.


If you want to prevent any rows from overlapping, put a variant of this query in a trigger:

create trigger mytable_no_overlap
before insert on mytable
for each row
begin
  if exists (select * from mytable
             where starttime <= new.endtime
             and endtime >= new.starttime) then
    signal sqlstate '45000' SET MESSAGE_TEXT = 'Overlaps with existing data';
  end if;
end;
Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • What if you have two events with the same name? You need a `UNIQUE` constraint somewhere here in your table, probably a \*gulp\* auto-incrementing ID. – Lightness Races in Orbit Jul 04 '11 at 13:30
  • I normally remove the `=` part of the operators as I don't think 10-11 "overlaps" with 11-12 for most scheduling purposes. – Tim Lehner Jul 08 '14 at 19:54
  • 01:00 - 05:00 does not seem to overlap with 04:00 - 24:00 (because mysql shows 24:00 as 00:00) – Timo Huovinen May 03 '16 at 07:38
  • does not work for this weird case either (but should): `"17:00:00-03:00:00", "14:00:00-01:00:00"`, the problem is that time loops, and it's hard to control what numbers it gives especially when your converting timezones – Timo Huovinen May 03 '16 at 08:45
  • @timo if you have scheduling that spans to or past midnight, convert the time to epoch seconds before comparing as per my query – Bohemian May 03 '16 at 08:59
  • @Bohemian Thank you for that idea, but I wanted to make it into a generic mysql function that accepts 4 time arguments, the problem is that`TIME` does not have epoch seconds since it's not a `DATETIME` – Timo Huovinen May 03 '16 at 09:43
  • @timo whatever you have, you can ultimately convert it somehow to sone absolute time-like value, eg `hours * 60 + minutes` – Bohemian May 03 '16 at 11:01
  • @Bohemian mysql already treats `TIME` type comparisons correctly, just like it would with integers, I don't see how it solves the problem with times being passed as 00:00 when it actually represents 24:00 or look at the other case `"17:00:00-03:00:00", "14:00:00-01:00:00"` becomes `SELECT 61200 <= 3600 and 10800 >= 50400` which is false, even though it overlaps – Timo Huovinen May 03 '16 at 11:30
  • @Bohemian Ok, got it working! :D The solution was to convert the time intervals into a datetime time format using `MOD` or `CAST` and then for the cases when the end time is before the start time simply add 24 hours (because that's what it actually is), I posted it as an answer to this question in case you are interested, thank you for the help. – Timo Huovinen May 03 '16 at 12:39
  • Awesome. Worked like a charm. – Sohil Mar 05 '18 at 04:52
  • It's awesome dude. But how to check if there is already a record that is overlapping a time slot. – Karan Malhotra Oct 21 '21 at 11:09
  • @KaranMalhotra Does the query at the top of my answer not find overlaps? If it's not finding overlaps, please describe the data it is not finding. – Bohemian Oct 22 '21 at 04:15
  • @Bohemian No, your code is awesome. It works smooth as butter. I misunderstood something there... and after realizing I forgot to delete the comment. Sorry for that. – Karan Malhotra Oct 22 '21 at 07:46
  • Is there no built in function in mysql to handle for this situation? Seems pretty common enough. – BenKoshy Apr 23 '23 at 23:50
  • @BenKoshy there is no native support in MySQL for finding time range collisions – Bohemian Apr 24 '23 at 00:14
3

I wanted a generic function to check if two time ranges for days overlap which would also work with cases where the schedule starts before midnight and ends after, like "17:00:00-03:00:00" and "14:00:00-01:00:00" should overlap, so I modified the solution by Bohemian

you use this function as follows

SELECT func_time_overlap("17:00:00","03:00:00", "14:00:00","01:00:00")

or in your case like this

SELECT *
FROM mytable a
JOIN mytable b ON (
    a.name != b.name 
    AND func_time_overlap(a.starttime, a.endtime, b.starttime, b.endtime)
);

Here is the function definition

CREATE FUNCTION `func_time_overlap`(a_start TIME, a_end TIME, b_start TIME, b_end TIME) 
RETURNS tinyint(1) 
DETERMINISTIC
BEGIN

-- there are only two cases when they don't overlap, but a lot of possible cases where they do overlap

-- There are two time formats, one is an interval of time that can go over 24 hours, the other is a daily time format that never goes above 24 hours
-- by default mysql uses TIME as an interval
-- this converts a TIME interval into a date time format

-- I'm not using `TIME(CAST(a_start AS DATETIME));` to convert the time interval to a time
-- because it uses the current day by default and might get affected by the timezone settings of the database, 
-- just imagine the next day having the DST change.
-- although the CAST should work fine if you use UTC

IF a_start >= 24 THEN 
    SET a_start = TIME(CONCAT(MOD(HOUR(a_start), 24),':',MINUTE(a_start),':',SECOND(a_start))); 
END IF;

IF b_start >= 24 THEN 
    SET b_start = TIME(CONCAT(MOD(HOUR(b_start), 24),':',MINUTE(b_start),':',SECOND(b_start))); 
END IF;

IF a_end > 24 THEN 
    SET a_end = TIME(CONCAT(MOD(HOUR(a_end), 24),':',MINUTE(a_end),':',SECOND(a_end))); 
END IF;

IF b_end > 24 THEN 
    SET b_end = TIME(CONCAT(MOD(HOUR(b_end), 24),':',MINUTE(b_end),':',SECOND(b_end))); 
END IF;


-- if the time range passes the midnight mark, then add 24 hours to the time
IF a_start >= a_end THEN 
    SET a_end = a_end + INTERVAL 24 HOUR; 
END IF;

IF b_start >= b_end THEN 
    SET b_end = b_end + INTERVAL 24 HOUR; 
END IF;

RETURN a_start < b_end AND a_end > b_start;


END

I'm not using TIME(CAST(a_start AS DATETIME)); to convert the time interval to a time because it uses the current day by default and might get affected by the timezone settings of the database, just imagine the next day having the DST change.

If your database is using UTC timezone (as it should) then you can use this

IF a_start >= 24 THEN 
    SET a_start = TIME(CAST(a_start AS DATETIME)); 
END IF;

IF b_start >= 24 THEN 
    SET b_start = TIME(CAST(b_start AS DATETIME)); 
END IF;

IF a_end > 24 THEN 
    SET a_end = TIME(CAST(a_end AS DATETIME));
END IF;

IF b_end > 24 THEN 
    SET b_end = TIME(CAST(b_end AS DATETIME));
END IF;
Community
  • 1
  • 1
Timo Huovinen
  • 53,325
  • 33
  • 152
  • 143
0

Try this, it works for me

SELECT * from Shedulles a 
where exists 
( select 1 from Shedulles b 
    where 
    a.ShedulleId != b.ShedulleId 
    and ( a.DateFrom between b.DateFrom and b.DateTo 
    or a.DateTo between b.DateFrom and b.DateTo 
    or b.DateFrom between a.DateFrom and a.DateTo ) 
    and a.DateFrom != b.DateTo 
    and b.DateFrom != a.DateTo 
);

Or this one

SELECT DISTINCT a.* FROM Shedulles a
JOIN Shedulles b 
    ON 
    a.ShedulleId != b.ShedulleId 
    and ( a.DateFrom between b.DateFrom and b.DateTo 
    or a.DateTo between b.DateFrom and b.DateTo 
    or b.DateFrom between a.DateFrom and a.DateTo ) 
    and a.DateFrom != b.DateTo 
    and b.DateFrom != a.DateTo 
Matas Lesinskas
  • 414
  • 6
  • 13
0

Try this:

declare @tempTbl table(RecID)

    insert into @tempTbl
    Select RecID
    from 
    (
    Select t.RecID from Table1 t,Table1 t1
    where t.StartTime between t1.StartTime AND t1.EndTime
    AND t.RecID <> t1.RecID  

    )
Ovais Khatri
  • 3,201
  • 16
  • 14
  • Actually that won't work. consider a start time *before* the range, but end time *in* the range... that's an overlap, but your query wouldn't find it – Bohemian Jul 04 '11 at 13:26
  • @Bohemian It would actually work: the query checks both ranges and for the _other_ range the query would actually give a result in that case – Jan M Jan 26 '18 at 13:59
0

Trivial in PHP. Sort the list by start time, then check consecutive entries for overlap:

$end[$k] > $start[$k+1]

The equivalent in MySQL is trickier unless you have MySQL 8.0 (or MariaDB 10.2) because of needing LEAD() or LAG() to look at adjacent rows.

Rick James
  • 135,179
  • 13
  • 127
  • 222