检查时间范围重叠,守望者问题[SQL]

时间:2009-04-23 14:08:21

标签: sql sql-server-2005 algorithm linq-to-sql tsql

我遇到了一个更大问题的障碍。

作为大型查询的一部分,我需要解决“守夜人”问题。 我有一张表,其中包含日程安排的变化:

ID | Start          | End
1  | 2009-1-1 06:00 | 2009-1-1 14:00
2  | 2009-1-1 10:00 | 2009-1-1 18:00
3  | 2009-2-1 20:00 | 2009-2-2 04:00
4  | 2009-2-2 06:00 | 2009-2-2 14:00

作为查询的一部分,我需要确定在给定时间范围内房间内是否至少有一名守望者。

因此,如果我将范围2009-1-1 06:00指定为2009-1-1 12:00,则结果为真,因为第1和第2轮合并以涵盖此时间段 - 实际上可以链接任意数量的轮班以保持小心。但是,如果我将2009-2-1 22:00检查为2009-1-2 10:00,则结果为false,因为第二天早上4点到6点之间有休息。

我想在LINQ中实现 ,或者在SQL Server(2005)中实现用户定义的函数,因为在这两种情况下,这只是更大查询逻辑的一部分。必须运行以识别需要注意的元素。真实数据集涉及与任何给定时间段相交的大约一百个移位记录,但并不总是覆盖整个范围。

我发现的最接近的是 How to group ranged values using SQL Server 对于数字范围,但它取决于在下一个范围开始之前结束的每个范围。如果我可以构建相同的手表统一视图,只考虑重叠的手表,那么检查是否涵盖了特定时间将是微不足道的。统一视图如下所示:

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

注意:通过拉动所有数据并在其上运行一些手动循环来实现整个过程相对容易,但这是当前的系统,并且由于轮班次数和时间而相当慢必须检查的范围。

4 个答案:

答案 0 :(得分:2)

这是一种像这样平整日期范围的方法

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

您必须比较每行中的之前的 日期,看看是否

  • 当前行的开始日期介于上一行的日期范围之间。
  • 当前行的结束日期属于下一行的日期范围。

alt text

使用上面的代码,实现UDF非常简单。

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

答案 1 :(得分:1)

无人看守的间隔显然是在观察期结束时或您正在检查的整个时间范围的开始处开始的。因此,您需要一个查询来选择此集合中没有重叠移位的所有元素。查询看起来像:

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
    )

如果这是非空的,那么你有一个无人看守的间隔。我怀疑它会以二次方式运行,所以它可能比“sort,fetch and loop”慢。

答案 2 :(得分:0)

一种方法是创建一个临时表,每个时间值需要检查一行(这是你的班次分辨率的函数)。

如果是几分钟,一天就会有60 * 24 = 1440行;一周约10K行。

然后SQL相对简单:

  

SELECT COUNT(1)
  来自#minutes m
  LEFT JOIN在s.start_time和s.end_time之间移动m.checktime   有COUNT(1)= 0

这样做的好处是能够显示同一时间有多少班次。

根据您描述的尺度,执行时间应该可以忽略不计。

答案 3 :(得分:0)

我正在查看日期范围,并认为我会重新访问这个问题。我可能会在这里黯然失色,但似乎这两个条件就够了

(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.

欣赏这可能不是最有效的。

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

结果集

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

实际上只需检查右邻居或左邻居,只要确保检查范围的开始和结束,这样就可以将范围的开始作为虚拟区间引入并按如下方式检查正确的邻居: -

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

结果集

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