3

有一个现有问题询问如何在忽略重叠的情况下查找多个日期范围内的分钟数。

给出的示例数据是(用户 ID 不是特别相关)

--Available--
ID  userID  availStart          availEnd
1   456     '2012-11-19 16:00'  '2012-11-19 17:00'
2   456     '2012-11-19 16:00'  '2012-11-19 16:50'
3   456     '2012-11-19 18:00'  '2012-11-19 18:30'
4   456     '2012-11-19 17:30'  '2012-11-19 18:10'
5   456     '2012-11-19 16:00'  '2012-11-19 17:10'
6   456     '2012-11-19 16:00'  '2012-11-19 16:50'

我可以使用游标解决问题,但我认为它应该适用于 CTE,但我不知道该怎么做。

方法是按开始时间排列每个范围然后我们构建一个按顺序合并范围的范围,直到找到一个不与我们合并的范围重叠的范围。然后我们计算在合并范围内有多少分钟,并记住这一点 我们继续下一个范围,再次合并任何重叠的范围。每次我们得到一个不重叠的起点时,我们都会累积分钟最后我们将累积的分钟添加到最后一个范围的长度上

很容易看出,由于顺序的原因,一旦一个范围与之前的范围不同,那么没有更多的范围可以与之前的范围重叠,因为它们的开始日期都更大。

Declare
  @UserID int = 456,
  @CurStart datetime, -- our current coalesced range start
  @CurEnd datetime, -- our current coalesced range end
  @AvailStart datetime, -- start or range for our next row of data
  @AvailEnd datetime, -- end of range for our next row of data
  @AccumMinutes int = 0 -- how many minutes so far accumulated by distinct ranges

Declare MinCursor Cursor Fast_Forward For
Select
  AvailStart, AvailEnd
From
  dbo.Available
Where
  UserID = @UserID
Order By
  AvailStart

Open MinCursor

Fetch Next From MinCursor Into @AvailStart, @AvailEnd
Set @CurStart = @AvailStart
Set @CurEnd = @AvailEnd

While @@Fetch_Status = 0
Begin
  If @AvailStart <= @CurEnd -- Ranges Overlap, so coalesce and continue
    Begin
    If @AvailEnd > @CurEnd 
      Set @CurEnd = @AvailEnd
    End
  Else -- Distinct range, coalesce minutes from previous range
  Begin
    Set @AccumMinutes = @AccumMinutes + DateDiff(Minute, @CurStart, @CurEnd)
    Set @CurStart = @AvailStart -- Start coalescing a new range
    Set @CurEnd = @AvailEnd
  End
  Fetch Next From MinCursor Into @AvailStart, @AvailEnd
End

Select @AccumMinutes + DateDiff(Minute, @CurStart, @CurEnd) As TotalMinutes

Close MinCursor
Deallocate MinCursor;

让 CTE 工作,只是递归中的一个愚蠢的错误。查询计划的爆炸式增长令人印象深刻:

With OrderedRanges as (
  Select
    Row_Number() Over (Partition By UserID Order By AvailStart) AS RN,
    AvailStart,
    AvailEnd
  From
    dbo.Available
  Where
    UserID = 456
),
AccumulateMinutes (RN, Accum, CurStart, CurEnd) as (
  Select
    RN, 0, AvailStart, AvailEnd
  From
    OrderedRanges
  Where 
    RN = 1
  Union All
  Select
    o.RN, 
    a.Accum + Case When o.AvailStart <= a.CurEnd Then
        0
      Else 
        DateDiff(Minute, a.CurStart, a.CurEnd)
      End,
    Case When o.AvailStart <= a.CurEnd Then 
        a.CurStart
      Else
        o.AvailStart
      End,
    Case When o.AvailStart <= a.CurEnd Then
        Case When a.CurEnd > o.AvailEnd Then a.CurEnd Else o.AvailEnd End
      Else
        o.AvailEnd
      End
  From
    AccumulateMinutes a
        Inner Join 
    OrderedRanges o On 
        a.RN = o.RN - 1
)

Select Max(Accum + datediff(Minute, CurStart, CurEnd)) From AccumulateMinutes 

这是否适用于 CTE,是否有一种以这种方式累积列表的通用模式?

http://sqlfiddle.com/#!6/ac021/2

4

2 回答 2

6

以下查询根据您的定义查找数据中的句点。它首先使用相关子查询来确定一条记录是否是一个时期的开始(即,与较早的时期没有重叠)。然后它将“periodStart”指定为最近的开始,即非重叠时段的开始。

以下(未经测试的)查询采用这种方法:

with TimeWithOverlap as (
     select t.*,
            (case when exists (select * from dbo.Available tbefore where t.availStart > tbefore.availStart and tbefore.availEnd >= t.availStart)
                  then 0
                  else 1
             end) as IsPeriodStart
     from dbo.Available t 
    ),
    TimeWithPeriodStart as (
     select two.*,
            (select MAX(two1.AvailStart) from TimeWithOverlap two1 where IsPeriodStart = 1 and two1.AvailStart <= two.AvailStart
            ) as periodStart
     from TimeWithOverlap two
    )
select periodStart, MAX(AvailEnd) as periodEnd
from TimeWithPeriodStart twps
group by periodStart;

http://sqlfiddle.com/#!6/3483c/20(第二次查询)

如果两个时段同时开始,那么它仍然有效,因为 AvailStart 值相同。由于相关的子查询,即使在中等大小的数据集上也可能表现不佳。

还有其他方法可以解决这个问题。例如,如果您有 SQL Server 2012,您将能够使用累积求和函数,它提供了一种更简单的方法。

于 2012-11-20T22:54:52.830 回答
0

我通过创建一个愚蠢的表(在某种程度上)非常有效地解决了这个问题,该表在一列(PK)中具有日期和时间(精确到分钟),而在第二列中则有一点。“1”表示用户可用,0 表示他/她不可用。

剩下的就很简单了。我厌倦了不得不编写无休止的复杂查询来尝试在部分重叠的时间范围内获取分钟数。

事实上,这是为了计算机的效率。

我知道这不是真正的交易,而是我想出的最简单的解决方案。您可以创建一个创建该表的函数/SP..

于 2012-11-20T22:37:24.977 回答