检查时间范围重叠,守望者问题 [SQL]
Posted
技术标签:
【中文标题】检查时间范围重叠,守望者问题 [SQL]【英文标题】:Checking for time range overlap, the watchman problem [SQL] 【发布时间】:2009-04-23 14:08:21 【问题描述】:我在一个更大的问题上遇到了障碍。
作为大型查询的一部分,我需要解决“守夜人”问题。 我有一张这样的日程表:
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
作为查询的一部分,我需要确定在给定时间范围内,房间内是否始终至少有 1 名守望者。
因此,如果我指定范围 2009-1-1 06:00
到 2009-1-1 12:00
,结果为真,因为班次 1 和班次 2 合并以涵盖此时间段 - 事实上,任何数量的班次都可以链接起来以保持监视。但是,如果我检查 2009-2-1 22:00
到 2009-1-2 10:00
,结果是错误的,因为第二天早上 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
注意:只需提取所有数据并在其上运行一些手动循环,整个事情就相对容易实现,但这是当前系统,由于班次和时间的数量,它相当慢必须检查的范围。
【问题讨论】:
不应该将“所以如果我指定范围 2009-1-1 12:00 到 2009-1-1 06:00”中的日期范围反转为“2009-1-1 06: 00 到 2009 年 1 月 1 日 12:00”? @David 您可能想要下载这本电子书:“在 SQL 中开发面向时间的数据库应用程序”(cs.arizona.edu/people/rts/tdbbook.pdf)。它有很多关于对具有日期范围的表进行复杂 SQL 查询的有用信息。 +1 - 我正在解决类似的问题,识别不相交和重叠 【参考方案1】:这是一种扁平化日期范围的方法
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
您必须比较每一行中的上一个 和 下一个日期,看看是否
当前行的开始日期介于上一行的日期范围之间。 当前行的结束日期介于下一行的日期范围之间。使用上面的代码,实现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
【讨论】:
例如,当我们有班次:12-2 和 4-6,我们检查 1-6。这应该会失败,因为 2-4 是空的。 非常好。具有讽刺意味的是,因为这是一个更大、更复杂问题的一部分,我找到了一种更快的方法来完成整个事情,方法是创建一种由触发器驱动的查找表,将大部分 CPU 成本转移到创建记录的时间(这比需要阅读和分析它们的频率要低得多)。但是,我必须在这里处理这些类型的重叠范围,所以这很快就会非常有用。 @David 我想知道您是否可以在我的代码中为“FlattenedDateRange”CTE 创建一个索引视图,而不是使用触发器。无论如何,很高兴听到您能够通过所选答案以外的其他方式解决问题。【参考方案2】:一个无人看守的间隔显然开始于观察周期的结束或您正在检查的整个时间范围的开始。因此,您需要一个查询来从该集合中选择没有重叠移位的所有元素。查询如下所示:
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
)
如果这是非空的,那么你有一个无人看守的间隔。我怀疑它会在二次时间内运行,所以它可能比“排序、获取和循环”慢。
【讨论】:
我在完成这项工作时遇到了一些麻烦。我假设您也打算在某个地方过滤结束时间,但是即使添加这一点,或者只是确保仅传递有效的班次(通过使班次成为视图),结果似乎与提供的范围无关 - 相同无论范围是在班次之内还是班次之外,都会给出结果。 对,顶部子查询完全忽略了范围 - 添加了检查【参考方案3】:一种方法是创建一个临时表,其中包含需要检查的每个时间值的一行(这是轮班分辨率的函数)。
如果是分钟,那么一天会有 60 * 24 = 1440 行;一周大约 10K 行。
那么SQL就比较简单了:
选择计数(1) 来自 #minutes m LEFT JOIN 在 s.start_time 和 s.end_time 之间转移 s 到 m.checktime 有 COUNT(1) = 0
这样做的好处是还可以显示同时覆盖了多少班次。
考虑到您描述的比例,执行时间应该可以忽略不计。
【讨论】:
【参考方案4】:我正在查看日期范围,并认为我会重新审视这个问题。在这里我可能会摔倒,但似乎这两个条件就足够了
(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
【讨论】:
以上是关于检查时间范围重叠,守望者问题 [SQL]的主要内容,如果未能解决你的问题,请参考以下文章