Checking for time range overlap, the watchman problem [SQL]

后端 未结 4 1696
故里飘歌
故里飘歌 2020-12-09 13:26

I am running into a road block on a larger problem.

As part of a large query I need to solve a \"night watchman\" problem. I have a table with schedule shifts as suc

相关标签:
4条回答
  • 2020-12-09 13:44

    Here is a way to flatten date range like this

    Start          | End
    2009-1-1 06:00 | 2009-1-1 18:00
    2009-2-1 20:00 | 2009-2-2 04:00
    2009-2-2 06:00 | 2009-2-2 14:00
    

    You have to compare previous and next dates in each row and see whether

    • Current row's Start date falls between previous row's date range.
    • Current row's End date falls between next row's date range.

    alt text

    Using above code, implementing UDF is as simple as followed.

    create function fnThereIsWatchmenBetween(@from datetime, @to datetime)
    returns bit
    as
    begin
        declare @_Result bit
    
        declare @FlattenedDateRange table (
            Start   datetime,
            [End]   datetime
        )
    
        insert  @FlattenedDateRange(Start, [End])
        select  distinct 
                Start = 
                    case 
                        when Pv.Start is null then Curr.Start 
                        when Curr.Start between Pv.Start and Pv.[End] then Pv.Start
                        else Curr.Start 
                    end,
                [End] = 
                    case 
                        when Curr.[End] between Nx.Start and Nx.[End] then Nx.[End] 
                        else Curr.[End] 
                    end
        from    shift Curr
                left join shift Pv on Pv.ID = Curr.ID - 1 --; prev
                left join shift Nx on Nx.ID = Curr.ID + 1 --; next
    
        if exists(  select  1
                    from    FlattenedDateRange R
                    where   @from between R.Start and R.[End]
                            and @to between R.Start and R.[End]) begin
            set @_Result = 1    --; There is/are watchman/men during specified date range
        end
        else begin
            set @_Result = 0    --; There is NO watchman
        end
    
        return @_Result
    end
    
    0 讨论(0)
  • 2020-12-09 13:53

    An unguarded interval obviously starts either at the end of a watched period or at the beginning of the whole time range that you are checking. So you need a query that selects all elements from this set that don't have an overlapping shift. The query would look like:

    select 1 
    from shifts s1 where not exists
        (select 1 from shifts s2
         where s2.start<=s1.end and s2.end > s1.end
        )
        and s1.end>=start_of_range and s1.end<  end_of_range
    union
    select 1 
    where not exists
        (select 1 from shifts s2 
          where s2.start<=start_of_range and s2.end > start_of_range
        )
    

    If this is non-empty, then you have an unguarded interval. I suspect it will run in quadratic time, so it might be slower than "sort, fetch and loop".

    0 讨论(0)
  • 2020-12-09 13:54

    I was looking at date ranges and thought I would re-visit this question. I may fall flat on my face here, but it seems these two conditions would be enough

    (1) Shift is not at beginning of range and has no left neighbour
    
    OR
    
    (2) Shift is not at end of range and has no right neighbour.
    

    Appreciate this may not be the most efficient.

    CREATE TABLE times
    (
    TimeID int,
    StartTime Time,
    EndTime Time
    )
    
    INSERT INTO times
    VALUES
    (1,'10:00:00','11:00:00'),
    (2,'11:00:00','12:00:00'),
    (3,'13:00:00','14:00:00'),
    (4,'14:30:00','15:00:00'),
    (5,'15:00:00','16:00:00'),
    (6,'16:00:00','17:00:00')
    
    declare @start_of_range time ='09:30:00'
    declare @end_of_range time = '17:30:00'
    
    
    
    select timeID,StartTime,EndTime 
    from times s1 where
    -- No left neighbour and not at beginning of range
       not exists
        (select 1 from times s2
         where s2.startTime < s1.startTime and s2.endTime >= s1.startTime
        )
        and s1.StartTime>@start_of_range
      or
    -- No right neighbour and not at end of range
       not exists
        (select 1 from times s2
         where s2.startTime <= s1.endTime and s2.endTime > s1.endTime
        )
        and s1.EndTime<@end_of_range
    

    Result set

    timeID  StartTime   EndTime
    1   10:00:00.0000000    11:00:00.0000000
    2   11:00:00.0000000    12:00:00.0000000
    3   13:00:00.0000000    14:00:00.0000000
    4   14:30:00.0000000    15:00:00.0000000
    6   16:00:00.0000000    17:00:00.0000000
    

    Actually it's only necessary to check either the right neighbours or the left neighbours, as long as you make sure that the start and end of range is checked, so you could introduce the start of range as a dummy interval and just check the right neighbours as follows:-

    select * from
    (
    select timeID,StartTime,EndTime 
    from times union select 0,@start_of_range,@start_of_range) s1
    where
        not exists
        (select 1 from times s2
         where s2.startTime<=s1.endTime and s2.endTime > s1.endTime
        )
        and s1.EndTime<@end_of_range
    

    Result set

    timeID  StartTime   EndTime
    0   09:30:00.0000000    09:30:00.0000000
    2   11:00:00.0000000    12:00:00.0000000
    3   13:00:00.0000000    14:00:00.0000000
    6   16:00:00.0000000    17:00:00.0000000
    
    0 讨论(0)
  • 2020-12-09 13:55

    One way is to create a temp table with a row for each time value requiring to be checked (which is a function of the resolution of your shifts).

    If it were minutes it would have 60 * 24 = 1440 rows for a day; about 10K rows for a week.

    Then the SQL is relatively simple:

    SELECT COUNT(1)
    FROM #minutes m
    LEFT JOIN shifts s ON m.checktime BETWEEN s.start_time AND s.end_time
    HAVING COUNT(1) = 0

    This has the benefit of also being able to show how many shifts are covering the same time.

    The execution time should be negligible given the scales you've described.

    0 讨论(0)
提交回复
热议问题