sqlcommon-table-expressionrecursive-queryoverlapgaps-and-islands

Identifying Overlapping Appointments with One Non-Overbooked Occurrence


Scenario:

Appointments are made with start and end times. Appointments are allowed to overlap, but for reporting purposes, we need to know when there are overlapping appointments (called overbooking) and when there are not. Two or more appointments may overlap with each other, but there's always one appointment involved in the overbooking that is not classified as overbooked.

Rules:

1. Overlapping portions (segments) of appointments are considered overbooked, except for one of the occurrences of the overlap, which is NOT considered overbooked.

2. The occurrence of the overlap that is chosen to not be considered overbooked does NOT matter; it can be any of the overlaps in the set.

Sample Data:

Appt ID Appt Start Appt End
1 2:00 5:00
2 3:00 6:00
3 2:00 6:00
4 6:00 7:00

Desired Results:

Appt ID Classification Segment Start Segment End
1 NOT OVERBOOKED 2:00 5:00
2 OVERBOOKED 3:00 5:00
2 NOT OVERBOOKED 5:00 6:00
3 OVERBOOKED 2:00 6:00
4 NOT OVERBOOKED 6:00 7:00

...or...

Appt ID Classification Segment Start Segment End
1 NOT OVERBOOKED 2:00 5:00
2 OVERBOOKED 3:00 6:00
3 OVERBOOKED 2:00 5:00
3 NOT OVERBOOKED 5:00 6:00
4 NOT OVERBOOKED 6:00 7:00

etc.

What I've tried:

DROP TABLE IF EXISTS #appts
CREATE TABLE #appts 
(
    appt_id INT 
    ,st TIME
    ,et TIME
)

INSERT INTO #appts (appt_id,st,et) VALUES (1,'2:00','5:00')
INSERT INTO #appts (appt_id,st,et) VALUES (2,'3:00','6:00')
INSERT INTO #appts (appt_id,st,et) VALUES (3,'2:00','6:00')
INSERT INTO #appts (appt_id,st,et) VALUES (4,'6:00','7:00')

;With a1 AS
(
    SELECT * FROM #appts
)
,a2 AS 
(
    SELECT * FROM a1
)

SELECT DISTINCT
    a1.appt_id
    ,a2.appt_id
    ,ost = CASE WHEN a2.st > a1.st THEN a2.st ELSE a2.st END
    ,oet = CASE WHEN a2.et < a1.et THEN a2.et ELSE a1.et END
FROM 
    a1
    INNER JOIN a2 
        ON (a1.st > a2.st
        AND a1.st < a2.et
        OR a1.et > a2.st
        AND a1.et < a2.et)
WHERE
    a1.appt_id < a2.appt_id
appt_id appt_id ost est
1 2 3:00 5:00
1 3 2:00 5:00
2 3 2:00 6:00

This does not get me one of the acceptable result sets. It appears to give me the possible overbookings, but I need it to also give me the "not overbooked", allowing for one of the overbookings in each overlap to count as not overbooked.


Solution

  • For MYSQL and comp.:

    with Appointments(Appt_ID, Appt_Start, Appt_End) as (
        select 1, cast('2:00' as time), cast('5:00' as time) union all
        select 2, cast('3:00' as time), cast('6:00' as time) union all
        select 3, cast('2:00' as time), cast('6:00' as time) union all
        select 4, cast('6:00' as time), cast('7:00' as time)
    )
    , slices(Appt_Start, Appt_End) as (
        select Appt_Start, Appt_End from (
            select dt as Appt_Start, lead(dt) over(order by dt, typ) as Appt_End
            from (
                select Appt_Start as dt, 1 as typ from Appointments
                union all 
                select Appt_End, -1 from Appointments
            ) d
        ) d
        where Appt_Start < Appt_End
    )
    select Appt_ID, classification, min(segment_Start) as segment_Start, max(segment_end) as segment_end
    from (
        select Appt_ID,
            case 
                when rn_by_slice = 1 then 'NOT OVERBOOKED' 
                else 'OVERBOOKED' end as classification,
            segment_Start, segment_end
        from (
            select d.Appt_ID, s.Appt_Start as segment_Start, s.Appt_End as segment_End,
                 d.Appt_Start, d.Appt_End,
                row_number() over(partition by s.Appt_Start, s.Appt_End order by d.Appt_ID) as rn_by_slice 
            from slices s
            join Appointments d
                on least(d.Appt_End - INTERVAL '1' SECOND, s.Appt_End - INTERVAL '1' SECOND) >= greatest(d.Appt_Start, s.Appt_Start)
        ) d
    ) d     
    group by Appt_ID, classification
    order by Appt_ID, segment_Start, segment_End
    ;
    

    https://dbfiddle.uk/xH2WhG49