Warning: file_get_contents(/data/phpspider/zhask/data//catemap/7/sql-server/25.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Sql 多个日期范围之间的分钟数总和_Sql_Sql Server_Sql Server 2008_Function_Date - Fatal编程技术网

Sql 多个日期范围之间的分钟数总和

Sql 多个日期范围之间的分钟数总和,sql,sql-server,sql-server-2008,function,date,Sql,Sql Server,Sql Server 2008,Function,Date,场景是用户指定何时可用,这些指定的时间可以相互重叠。我想知道他们的总可用时间。SQL Fiddle的示例: --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-1

场景是用户指定何时可用,这些指定的时间可以相互重叠。我想知道他们的总可用时间。SQL Fiddle的示例:

--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'
输出应为130分钟:

1: 60
2: 0 as falls inside 1
3: 30
4: 30 as the last 10 mins is covered by 3
5: 10 as first 60 mins is covered by 1
6: 0 as falls inside 1
我可以得到总重叠分钟数,但这超过了可用分钟数的总和:

有什么办法可以做到这一点吗

编辑11月12日21日:感谢大家的解决方案——在某种程度上,我很高兴看到这不是一个容易写的查询


编辑:这都是伟大的作品。在内部,我们认为最好是确保用户不能输入重叠时间,例如强迫他们修改现有条目

主要的问题是,您可能有一系列重叠的条目,因此您需要无限次地组合以删除所有重叠-这比SQL更适合于过程方法。但是,如果您不想使用临时表,这里有一个CTE方法——请记住,CTE只能递归给定的次数,因此如果您有任何特别长的链,它将失败

Create Table #Available (
  ID int not null primary key,
  UserID int not null,
  AvailStart datetime not null,
  AvailEnd datetime not null
)


Insert Into #Available (ID,UserID, AvailStart, AvailEnd) Values
  (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'),
  (7,457, '2012-11-19 16:00', '2012-11-19 17:10'),
  (8,457, '2012-11-19 16:00', '2012-11-19 16:50');  
Select Distinct UserID 
into #users
from #Available


Create Table #mins(UserID int,atime datetime,aset tinyint )
Declare @start Datetime
Declare @end Datetime

Select @start=min(AvailStart),@end=max(AvailEnd) from #Available 
While @start<@end
    begin
     insert into #mins(UserID,atime) 
     Select UserID ,@Start from #users
     Select @start=DateAdd(mi,1,@start)
    end

update #mins set aset=1
from #Available
where atime>=AvailStart and atime<Availend and #mins.UserID = #Available.UserID


select UserID,SUM(aset) as [Minutes] 
from #mins
Group by UserID 
Drop table #Available
Drop table #mins
Drop table #users
WITH MergedAvailable
AS
(
  SELECT Available.UserID, Available.AvailStart, MAX(Available.AvailEnd) AS AvailEnd
    FROM Available
   WHERE (
           SELECT COUNT(*)
             FROM Available AS InnerAvailable
            WHERE InnerAvailable.AvailStart < Available.AvailStart
                  AND
                  InnerAvailable.AvailEnd >= Available.AvailStart
         ) = 0
   GROUP BY Available.UserID, Available.AvailStart
  UNION ALL
  SELECT MergedAvailable.UserID, MergedAvailable.AvailStart,
         LongestExtensionToAvailableInterval.NewIntervalEnd
    FROM MergedAvailable
   CROSS APPLY GetLongestExtensionToAvailableInterval(MergedAvailable.UserID,
               MergedAvailable.AvailStart,
               MergedAvailable.AvailEnd) AS LongestExtensionToAvailableInterval
   WHERE LongestExtensionToAvailableInterval.NewIntervalEnd IS NOT NULL
)

SELECT SUM(DATEDIFF(MINUTE,
                    FinalAvailable.AvailStart,
                    FinalAvailable.AvailEnd)) AS MinsAvailable
  FROM (
         SELECT MergedAvailable.UserID, MergedAvailable.AvailStart,
                MAX(MergedAvailable.AvailEnd) AS AvailEnd
           FROM MergedAvailable
          GROUP BY MergedAvailable.UserID, MergedAvailable.AvailStart
       ) AS FinalAvailable
最后通过递归添加的唯一行是:

456 2012-11-19 17:30 2012-11-19 18:30
为了便于示例,假设您有一个ID为7的行,从18:20到19:20。然后将出现第二个递归,它将返回该行:

456 2012-11-19 17:30 2012-11-19 19:20

因此,在查询将到达每个重叠范围的开始和结束时,它还将返回所有中间阶段。这就是为什么我们需要为CTE之后的每个开始日期获取总的最大结束日期,以删除它们。

这里有另一种使用光标的方法。我觉得这项技术应该适用于CTE,但我不知道怎么做

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

很容易看出,由于顺序的关系,一旦某个范围与 之前发生的事情,没有更多的范围可以与之前发生的事情重叠,因为 开始日期都更大

Declare
  @UserID int = 456,
  @CurStart datetime, -- our current coalesced range start
  @CurEnd datetime, -- our current coalsced 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;
条件t1.availStart>t2.availEnd或t1.availEnd
Probably more than one crossing period.
In your case it 
16:00:00 - 17:10:00 includes the ranges:16:00:00 - 16:50:00,
                                        16:00:00 - 16:50:00,
                                        16:00:00 - 17:00:00,
                                        16:00:00 - 17:10:00
17:30:00 - 18:30:00 includes the ranges:17:30:00 - 18:10:00,
                                        18:00:00 - 18:30:00
2012年11月21日更新;30.11.2012; 2013年1月4日

CREATE FUNCTION dbo.Overlap
 (
  @availStart datetime,
  @availEnd datetime,
  @availStart2 datetime,
  @availEnd2 datetime
  )
RETURNS TABLE
RETURN
  SELECT CASE WHEN @availStart >= @availEnd2 OR @availEnd <= @availStart2
              THEN @availStart ELSE
                               CASE WHEN @availStart > @availStart2 THEN @availStart2 ELSE @availStart END
                               END AS availStart,
         CASE WHEN @availStart >= @availEnd2 OR @availEnd <= @availStart2
              THEN @availEnd ELSE
                             CASE WHEN @availEnd > @availEnd2 THEN @availEnd ELSE @availEnd2 END
                             END AS availEnd

;WITH cte AS
 (
  SELECT userID, availStart, availEnd, ROW_NUMBER() OVER (PARTITION BY UserID ORDER BY AvailStart) AS Id
  FROM dbo.test53
  ), cte2 AS
 (
  SELECT Id, availStart, availEnd
  FROM cte
  WHERE Id = 1
  UNION ALL
  SELECT c.Id, o.availStart, o.availEnd
  FROM cte c JOIN cte2 ct ON c.Id = ct.Id + 1
             CROSS APPLY dbo.Overlap(c.availStart, c.availEnd, ct.availStart, ct.availEnd) AS o
  )
  SELECT TOP 1 SUM(DATEDIFF(minute, availStart, MAX(availEnd))) OVER()
  FROM cte2
  GROUP BY availStart
上的演示有一个

我对所有的工作算法都做了一些研究 空白值表示花费的时间太长。这是在一个单核i7 X920@2GHz芯片上测试的,该芯片由两个SSD支持。创建的唯一索引是用户ID AvailStart上的集群。如果你认为你可以提高任何性能,让我知道

此CTE版本比线性版本更糟糕,SQL Server无法以有效的方式执行RN=RN+1连接。我用下面的混合方法纠正了这个问题,我将第一个CTE保存并索引到一个表变量中。这仍然需要10倍于基于光标的方法的IO

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/table变量版本,除了基于游标的方法之外,它的性能比其他任何方法都好

Create Function dbo.AvailMinutesHybrid(@UserID int) Returns Int As
Begin

Declare @UserRanges Table (
  RN int not null primary key, 
  AvailStart datetime, 
  AvailEnd datetime
)
Declare @Ret int = Null

;With OrderedRanges as (
  Select
    Row_Number() Over (Partition By UserID Order By AvailStart) AS RN,
    AvailStart,
    AvailEnd
  From
    dbo.Available
  Where
    UserID = @UserID
)
Insert Into @UserRanges Select * From OrderedRanges


;With AccumulateMinutes (RN,Accum, CurStart, CurEnd) as (
  Select
    RN, 0, AvailStart, AvailEnd
  From
    @UserRanges
  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 
    @UserRanges o On 
        a.RN + 1 = o.RN
)

Select 
  @Ret = Max(Accum + datediff(Minute, CurStart, CurEnd)) 
From 
  AccumulateMinutes 
Option
  (MaxRecursion 0)

Return @Ret

End

谢谢-我知道你是如何做到这一点的,在有时间的时候插入个人分钟。我希望有另一种方法可以做到这一点,而不必完全使用临时表。希望你也是这样,所以问题是,对不起,我的英语不好,这两种方法之间不仅会有重叠,还会有空闲时间。非常感谢这个解决方案,这对我来说是最简单的,但是我希望避免临时表!Alexander Fedorenko有一个你需要的问题:-我提出了一个关于游标方法是否可以转换为CTE的单独问题。我对目前为止所有的算法进行了性能分析,所以你要寻找连续分钟的总和,或者给定集合的分钟总和。找到最小最大分钟数并减去未分配的分钟数可能会更容易。@DanAndrews它更像是日期范围并集中的总分钟数。找到漏洞似乎和找出重叠点一样困难。代码当然会说话@Laurence你是在自己的服务器上进行比较的吗?SQLFiddle报告的运行时间比那些运行时间要高!如果您按开始时间对范围进行排序,则只需通过一次。合并直到找到一个不相交的范围,然后从上一个合并范围中累积分钟数。我不确定这是否有助于简化CTE。我不确定你指的是什么,抱歉?理论上,你可以在开始的时间顺序上运行CTE,但我不认为它会运行得很好,它肯定不会是一次通过。看到了吗
我的答案是使用游标,它维护一个累加器,并且每行只遍历一次。不过,我没能把它换成CTE。我不知道这是否会让它变得更简单。CTEs不能像过程方法那样在单个行上运行,它需要为每个行运行单独的查询。这也意味着所有参与的行都将显示为结果,并且您需要忽略除最后一行之外的所有行,假设您正在整个结果集中累积值。此查询中的某些内容可能会导致计划爆炸。例如,见。这似乎与间隔的密度有关,如果我将它们分散在一年内,我可以很高兴地得到数千行的结果。除了userid和availstart之外,还有其他索引方案可以帮助我吗?谢谢Laurence,我几乎可以了解这一点了。这个“小”问题是计算用户可用时间的更大SQL查询的一部分-如果我走这条路线,我将使用CTE或将其包装在表函数中。您可以像这样将其包装在UDF中。但是,如果你在大的结果集上调用它,我不确定这是否是一个好主意。谢谢Alexander,这看起来很棒。你能解释一下为什么OVER子句很重要吗?这很好,但是如果你有如下范围,例如:17:00-18:00,17:00-17:10,17:50-18:00,它就不起作用了。从整体上看,它们都共享分钟,但第二和第三个范围并不重叠。我对解决此问题的各种查询和查询计划感到惊讶。新解决方案的计划相当可怕!干得好,劳伦斯。你是伟大的;我更新了我的答案。这仍然不起作用,我在更大的数据集上做了一些计时,它得到了与其他查询不同的答案。这是我能找到的最简单的例子:这很奇怪,因为删除任何一行似乎都能让它工作,而且时间只有连续六个小时……感谢你指出我答案中的一个缺陷。我不确定它是否可以在不改变方法的情况下进行修改,所以我刚刚删除了它。同时,我认为你的两个答案都表明,用纯粹的基于集合的方法很难解决这个问题。我的意思是,递归CTE本质上是一个迭代,只是作为单个查询的形式更为恰当。诚然,Gordon提出了一个基于集合的解决方案,但他不得不两次引用同一个不太轻的CTE,这是我试图避免的事情,最终未能解释所有可能的情况。无论如何,两种方法都是解决方案,所以我都投了票。我放弃。你的方法最好+两个答案各1个。顺便说一下,我更新了答案。谢谢
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 
Create Function dbo.AvailMinutesHybrid(@UserID int) Returns Int As
Begin

Declare @UserRanges Table (
  RN int not null primary key, 
  AvailStart datetime, 
  AvailEnd datetime
)
Declare @Ret int = Null

;With OrderedRanges as (
  Select
    Row_Number() Over (Partition By UserID Order By AvailStart) AS RN,
    AvailStart,
    AvailEnd
  From
    dbo.Available
  Where
    UserID = @UserID
)
Insert Into @UserRanges Select * From OrderedRanges


;With AccumulateMinutes (RN,Accum, CurStart, CurEnd) as (
  Select
    RN, 0, AvailStart, AvailEnd
  From
    @UserRanges
  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 
    @UserRanges o On 
        a.RN + 1 = o.RN
)

Select 
  @Ret = Max(Accum + datediff(Minute, CurStart, CurEnd)) 
From 
  AccumulateMinutes 
Option
  (MaxRecursion 0)

Return @Ret

End