SQL query to find available slots with multiple providers and users

Viewed 47

I want to be able to find the number of available slots for a particular time duration for all locations and all days

For example: I have to know the number of available appointments before 10 AM in all locations from the below sample tables

I have looked at other answers in stack overflow, mine is peculiar in the sense it also involves data on multiple doctors/patients.

Doctor's time table

Location RESOURCE Day StartTime EndTime
ABC D1 Mon 8:00 AM 12:00 PM
ABC D1 Tue 8:00 AM 12:00 PM
ABC D2 Mon 9:00 AM 01:00 PM
ABC D2 Tue 8:00 AM 12:00 PM
XYZ D1 Mon 8:00 AM 12:00 PM
XYZ D1 Tue 8:00 AM 12:00 PM
XYZ D4 Mon 9:00 AM 01:00 PM
XYZ D4 Tue 8:00 AM 12:00 PM

Patient's appointment time table

Location Patient Duration StartTime ApptDt
ABC P1 15 8:00 AM 10/4/2021
ABC P2 15 8:15 AM 10/4/2021
ABC P3 15 9:00 AM 10/4/2021
ABC P4 15 9:00 AM 10/5/2021
XYZ P5 15 10:00 AM 10/5/2021
XYZ P6 15 10:00 AM 10/5/2021
XYZ P7 15 10:15 AM 10/5/2021
XYZ P8 15 10:15 AM 10/5/2021

Doctor's time table does not have dates as it is the same throughout the year.

On Mondays in ABC location, since there are 2 doctors overlapping the time between 9:00 AM to 12:00 noon, they can accept multiple appointments at the same time. ie, 2 patients from 9:00 am to 9:15 am can be served in location ABC.

A typical duration(Duration) for an appointment is 15 minutes as indicated in the patient's table.

Expected result set

Location Date Available appts
ABC. 10/4/2021 8
XYZ 10/4/2021 12

On 10/4/2021 there were 8 slots available for booking before 10 AM because there were no appointments between

  1. 8:30-8:45 for D1
  2. 8:45-9:00 for D1
  3. 9:00-9:15(2) for D1,D2
  4. 9:15-9:30(2) for D1,D2
  5. 9:30-9:45(2) for D1,D2
  6. 9:45-10:00(2) for D1,D2

I want to also know for a specific time slot how many appointments were booked vs available.

1 Answers

I'd re-imagine this data as transactional using CTEs, compute balances and then find the points where the balance is non-zero.

Conceptually, that means there's a +1 doctor transaction on each doctor's start time, and a -1 doctor transaction on each doctor's end time. Patients are just the reverse, there is a -1 doctor transaction at their start time and a +1 doctor transaction at their start time plus duration.

So something like:

WITH DrStarts AS (
    SELECT 
        1 [Drs], 
        [Dates].[Date] + [DrSched].StartTime [Timestamp]
    FROM        [DrSched]
    INNER JOIN  [Dates]
    ON          WEEKDAY([Dates]) = [DrSched].[Day]
), DrEnds AS (
    SELECT 
        -1 [Drs], 
        [Dates].[Date] + [DrSched].EndTime [Timestamp]
    FROM        [DrSched]
    INNER JOIN  [Dates]
    ON          WEEKDAY([Dates]) = [DrSched].[Day]
), ApptStarts AS (
    SELECT -1 [Drs], [Date] + [Time] FROM [Appts]
), ApptEnds AS (
    SELECT -1 [Drs], DATEADD(MM,[Duration],[Date] + [Time]) FROM [Appts]
), Txns AS (
                SELECT *, 1 Priority FROM DrStarts 
    UNION ALL   SELECT *, 1 Priority FROM DrEnds 
    UNION ALL   SELECT *, 0 Priority FROM ApptStarts 
    UNION ALL   SELECT *, 0 Priority FROM ApptEnds 
)

I added priorities at the end so we can make sure the patient leaves an instant before the doctor leaves. Then you can get the balance using a windowed function like so:

, AvailDrs AS (
    SELECT 
        *,
        SUM([Drs]) OVER( ORDER BY [Timestamp] DESC, [Priority] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) [AvailDrs]
    FROM Txns 
)

Then to get the available slots, you just do:

SELECT 
    [AvailDrs].[Timestamp] [From],
    LEAD([AvailDrs].[Timestamp]) OVER(ORDER BY [AvailDrs].[Timestamp]) [To],
    [AvailDrs].[AvailDrs]
FROM AvailDrs 
WHERE [AvailDrs] > 0

Though you may want to filter that to get rid of zero-length windows because those will occur.

This is not very performant, but if you have a high volume scenario, you probably want to reconsider your database design to make this function require less transformation.

You also need to make a date table. I presume you actually have a work calendar somewhere, but if not there are myriad ways to create a date table within a dynamic start/end date so I just assume it exists here. this approach also lets you easily slot in holidays, and perhaps incorporate a dr-specific leave calendar too.

In general, a wide range of difficult SQL probnlems become much easier if you reimagine the data as account/amount/timestamp transactions. Here you don't even subdivide into accounts but you often need that concept for other puzzles.

Also, I haven't tested this exact code, so you may end up with duplicates. If that's the case you may need to global key ORDER BY tie breaker to keep everything running smooth in the windowed functions. You can add this as an identity column to both tables, or just define a CTE with a DENSE_RANK() key column and use that instead of selecting from the tables directly.

Related