选择重叠时间范围

时间:2009-08-13 13:03:07

标签: sql sql-server datetime

T-SQL DateTime问题。

我有一组时间范围。在这些时间范围内,可能存在一组重叠的时间范围,我将其称为“阻止”时间。封锁时间不会超过一天。我想要做的是分开时间以排除被阻止的时间,基本上给我时间范围不被“阻止”。可以安全地假设阻塞时间不能超出时间范围。

示例:我上午9点到下午5点工作,下午1点午休30分钟。我想要2行的结果:上午9点到下午1点和下午1点30分到下午5点。

如上所述,我有一组时间范围,所以在上面的例子中,工作时间可能每天都有所不同,休息次数和持续时间可能不同。

我想在SQL方面,输入参数如下所示:

declare @timeranges table ( StartDateTime datetime, EndDateTime datetime )
declare @blockedtimes table ( StartDateTime datetime, EndDateTime datetime )

insert into @timeranges 
select '01 Jan 2009 09:00:00', '01 Jan 2009 17:00:00'
union select '02 Feb 2009 10:00:00', '02 Feb 2009 13:00:00'

insert into @blockedtimes 
select '01 Jan 2009 13:00:00', '01 Jan 2009 13:30:00'
union select '02 Feb 2009 10:30:00', '02 Feb 2009 11:00:00'
union select '02 Feb 2009 12:00:00', '02 Feb 2009 12:30:00'

结果集看起来像这样。

Start                   End
---------------------   ---------------------
'01 Jan 2009 09:00:00' '01 Jan 2009 13:00:00'
'01 Jan 2009 13:30:00' '01 Jan 2009 17:00:00'
'02 Feb 2009 10:00:00' '02 Feb 2009 10:30:00'
'02 Feb 2009 11:00:00' '02 Feb 2009 12:00:00'
'02 Feb 2009 12:30:00' '02 Feb 2009 13:00:00'

我可以使用游标或while循环执行此操作,但如果有人可以建议如何在没有迭代的情况下执行此操作,那将非常棒 - 谢谢。

4 个答案:

答案 0 :(得分:2)

首先,可能有一些问题,但我会继续努力。
适用于给定数据,只需要尝试其他方案

declare @timeranges table ( StartDateTime datetime, EndDateTime datetime )
declare @blockedtimes table ( StartDateTime datetime, EndDateTime datetime )

insert into @timeranges 
select '01 Jan 2009 09:00:00', '01 Jan 2009 17:00:00'
union select '02 Feb 2009 10:00:00', '02 Feb 2009 13:00:00'
--union select '03 Feb 2009 10:00:00', '03 Feb 2009 15:00:00'


insert into @blockedtimes 
select '01 Jan 2009 13:00:00', '01 Jan 2009 13:30:00'
union select '02 Feb 2009 10:30:00', '02 Feb 2009 11:00:00' 
union select '02 Feb 2009 12:00:00', '02 Feb 2009 12:30:00'

--build an ordered, time range table with an indicator
--to determine which ranges are timeranges 'tr'
--and which are blockedtimes 'bt'
--
declare @alltimes table (row int, rangetype varchar(10), StartDateTime datetime, EndDateTime datetime )
insert into @alltimes
select
    row_number() over (order by a.startdatetime), *
from
    (
    select 'tr' as rangetype ,startdatetime, enddatetime from @timeranges
    union
    select 'bt' as rangetype ,startdatetime, enddatetime from @blockedtimes
    )a

--what does the data look like  
--
select * from @alltimes


--
-- build up the results
select
    --start time is either the start time of a timerange, or the end of a blockedtime
    case 
        when at1.rangetype = 'tr' then at1.startdatetime
        when at1.rangetype = 'bt' then at1.enddatetime 
    end as [Start],
    case 
        --a time range followed by another time range : end time from the current time range
        when at1.rangetype = 'tr' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'tr'
            then at1.enddatetime

        --a time range followed by nothing (last record) : end time from the currenttime range
        when at1.rangetype = 'tr' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) is null
            then at1.enddatetime

        --a time range followed by a blockedtime : end time is start time of blocked time
        when at1.rangetype = 'tr' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'bt'
            then (select top 1 at2.startdatetime from @alltimes at2 where at2.row > at1.row and at2.rangetype = 'bt' order by row)

        --a blocked time followed by a blockedtime : end time is start time of next blocked time    
        when at1.rangetype = 'bt'  and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'bt'
            then (select top 1 at2.startdatetime from @alltimes at2 where at2.row > at1.row and at2.rangetype = 'bt' order by row)

        --a blocked time followed by a time range : end time is end time of previous time range     
        when at1.rangetype = 'bt'  and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'tr'
            then (select top 1 at2.enddatetime from @alltimes at2 where at2.row < at1.row and at2.rangetype = 'tr' order by row desc)

        --a blocked time followed by nothing (last record) : end time is end time of previous time range    
        when at1.rangetype = 'bt'  and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) is null
            then (select top 1 at2.enddatetime from @alltimes at2 where at2.row < at1.row and at2.rangetype = 'tr' order by row desc)

    end as [End]

from @alltimes at1

答案 1 :(得分:1)

我以为我会分享我最终解决的解决方案:

对临时表进行轻微调整,因为我已经为@timeranges和@blockedtimes添加了一个StartDate字段

declare @timeranges table ( StartDate datetime, StartDateTime datetime, EndDateTime datetime ) 
declare @blockedtimes table ( StartDate datetime, StartDateTime datetime, EndDateTime datetime )

无论如何看起来比其他一些答案更简单 - 为每个人的欢呼提供帮助:)

select 
    *
from
(
    -- first SELECT get start boundry
    select t.StartDateTime s, b.StartDateTime e
    from @timeranges t, @blockedtimes b
    where 
        -- same day and blocks overlaps timerange
        t.StartDate = b.StartDate and (t.StartDateTime <= b.EndDateTime and b.StartDateTime <= t.EndDateTime)
    and
        -- the following is the important bit for this SELECT   
        not exists (select 1 from @blockedtimes b2 where b2.StartDate = b.StartDate and b2.StartDateTime < b.StartDateTime)
union
    -- second SELECT get spikes ie middle
    select b1.EndDateTime s, b2.StartDateTime e
    from @timeranges t, @blockedtimes b1, @blockedtimes b2
    where 
        -- same day and blocks overlaps timerange
        t.StartDate = b1.StartDate and (t.StartDateTime <= b1.EndDateTime and b1.StartDateTime <= t.EndDateTime) 
    and 
        -- same day and blocks overlaps timerange
        t.StartDate = b2.StartDate and (t.StartDateTime <= b2.EndDateTime and b2.StartDateTime <= t.EndDateTime) 
    and 
        -- the following is the important bit for this SELECT
        b1.EndDateTime < b2.StartDateTime
union
    -- third SELECT get end boundry
    select b.EndDateTime s, t.EndDateTime e
    from @timeranges t, @blockedtimes b
    where 
        -- same day and blocks overlaps timerange
        t.StartDate = b.StartDate and (t.StartDateTime <= b.EndDateTime and b.StartDateTime <= t.EndDateTime)
    and 
        -- the following is the important bit for this SELECT
        not exists (select 1 from @blockedtimes b2 where b2.StartDate = b.StartDate and b2.StartDateTime > b.StartDateTime)
) t1

答案 2 :(得分:0)

如果输入数据有两个条件,则此解决方案应该有效:A)每个阻塞时间间隔都在一个时间范围内。 (你说这可以假设。)和B)被阻止的时间间隔不重叠 - 也就是说,没有时间被一个以上的阻塞间隔“双重阻挡”。

    with TB(src,S,E) as (
      select 'T', StartDateTime, EndDateTime from @timeranges as T
      union all
      select 'B', StartDateTime, EndDateTime from @blockedtimes as B
    ), TBP(evt,switch,DT,rk) AS (
      select
        src+DT,
        CHARINDEX(src+DT,'TEBSTSBE')/5 AS OffOn,
        EventDT,
        row_number() over (
          order by EventDT, CHARINDEX(src+DT,'TEBSTSBE')/5 desc
        ) as rk
      from TB UNPIVOT (
        EventDT FOR DT in ([S],[E])
      ) as U
    )
      select
        min(DT) as StartDateTime,
        max(DT) as EndDateTime
      from TBP
      group by (rk-1)/2
      having min(DT) < max(DT)
      order by (rk-1)/2;

这是如何运作的?

它首先用0或1标记所有日期时间值,以指示可用性是否结束(0,对于@timeranges中的EndDateTime值和@blockedtimes中的StartDateTime值)或者开始(对于其他两种可能性为1)特定的时间。然后时间和标签按时间顺序排列,标记并用使用row_number函数的列rk编号。使用CASE表达式可以使标记更具可读性,但是键入的CHARINDEX更少...

由于这些假设,标签序列将在0和1:0,1,0,1,0,1 ...之间交替,每个连续的(0,1)对指示间隔的开始和结束可用性。这些间隔可以用(rk-1)/ 2编号。

行在每个可用性间隔上进行分组。组中的最小日期时间是开始时间,最大值是结束时间,如果这些不同,则组表示属于结果集的非空间隔。请注意,对于您的数据,没有空的间隔,但是如果两个阻塞时间相邻或者阻塞时间与时间范围同时结束,则会存在。

最后,结果以您想要的显示格式进行旋转。

这不是最容易阅读的代码,但它可能值得迷惑。像这样使用row_number和分组的解决方案有时可以解决棘手的问题。

答案 3 :(得分:0)

SELECT  COALESCE(bt.StartDateTime, tr.StartDateTime),
        bt.EndDateTime
FROM    @timeranges tr
CROSS APPLY
        (
        SELECT  bp.StartDateTime, bt.StartDateTime AS EndDateTime
        FROM    (
                SELECT  StartDateTime
                FROM    @blockedtimes bt
                WHERE   bt.EndDateTime >= tr.StartDateTime
                        AND bt.StartDateTime <= tr.EndDateTime
                UNION ALL
                SELECT  tr.EndDateTime
                ) bt
        OUTER APPLY
                (
                SELECT  TOP 1 EndDateTime AS StartDateTime
                FROM    @blockedtimes bti
                WHERE   bti.EndDateTime >= tr.StartDateTime
                        AND bti.StartDateTime <= tr.EndDateTime
                        AND bti.StartDateTime < bt.StartDateTime
                ORDER BY
                        bti.StartDateTime DESC
                ) AS bp
        ) bt

此解决方案依赖于以下假设:

  • 时间范围永远不会与其他时间范围重叠
  • 阻止的时间永远不会与其他阻止时间重叠