查找ID为开头/结束日期范围大于20天的ID

时间:2014-06-06 20:37:28

标签: sql sql-server-2008 date

我需要在第一个StartDate和最后一个EndDate之间找到所有在日期范围之外有20天或更长时间的ID。

一个ID有多个开始日期和结束日期。在以下示例中,Id 1有两个间隙,每个间隔少于20天。它应被视为从2012年10月1日到2014年10月30日的一个范围,没有任何差距。

1   10/01/2012  02/01/2013
1   01/01/2013  01/31/2013
1   02/10/2013  03/31/2013
1   04/15/2013  10/30/2014

Id 2在结束日期01/30/2013和开始日期05/01/2013之间的差距超过20天,因此必须由查询捕获。

2   01/01/2013  01/30/2013
2   05/01/2013  06/30/2014
2   07/01/2013  02/01/2014

Id 3应被视为2012年1月1日至2014年1月6日期间的一个范围,没有任何差距。结果日期02/28/2013和开始日期07/01/2013之间的差距应该被忽略,因为从2012年1月1日到2014年1月1日的范围涵盖了差距。

3   01/01/2012  01/01/2014
3   01/01/2013  02/28/2013
3   07/01/2013  06/01/2014

光标可以做到,但它的工作速度非常慢,是不可接受的。

SQL小提琴:http://sqlfiddle.com/#!3/27e3f/2/0

3 个答案:

答案 0 :(得分:2)

这是一个不使用游标的解决方案。我不知道它对大型数据集的速度有多快,所以希望你可以根据基于光标的方法测试它,让我知道它是如何保持的。有关代码的更多详细解释。

-- Get a list of all dates on which coverage starts or stops.
with [EventsCTE] as
(
    select [id], [startdate] as [date], 1 as [change] from dateranges
    union all
    select [id], [enddate] as [date], -1 as [change] from dateranges
),

-- Give each event a sequence number (by date) within its id.
[SequencedEventsCTE] as
(
    select row_number() over (partition by [id] order by [date]) as [seq], *
    from [EventsCTE]
),

-- Use the sequence number to construct a running total of the number of active
-- date ranges at each point in time.
[RunningTotalsCTE] as
(
    -- Base case: Get the first event for each id.
    select *, [change] as [rangesActive] 
    from [SequencedEventsCTE] where [seq] = 1

    union all

    -- Recursive case: build a running total for subsequent events.
    select [this].*, [this].[change] + [prev].[rangesActive] as [rangesActive]
    from [SequencedEventsCTE] [this]
        inner join [RunningTotalsCTE] [prev] on
            [this].[Id] = [prev].[Id] and
            [this].[seq] = [prev].[seq] + 1
),

-- Join each event to its successor and look for dates on which no range was 
-- active. This gives us a list of gaps and their sizes.
[GapsCTE] as
(
    select [gapStart].[Id], 
        datediff(day, [gapStart].[date], [gapEnd].[date]) as [GapSize]
    from [RunningTotalsCTE] [gapStart]
        inner join [RunningTotalsCTE] [gapEnd] on
            [gapStart].[Id] = [gapEnd].[Id] and
            [gapStart].[seq] = [gapEnd].[seq] - 1 and
            [gapStart].[rangesActive] = 0
)

-- Get the ids having gaps of 20 days or more.
select distinct [id] from [GapsCTE] where [GapSize] >= 20;

首先,在EventsCTE中,我将原始表格中的每一行拆分为两个"事件",一个表示日期范围已开始(这些记录包含change = 1 ),和一个表示日期范围已结束(change = -1)。从这开始似乎是必要的,因为你有重叠的范围;我只是通过将原始表中的一条记录与其后的记录进行比较来识别间隙。

SequencedEventsCTE获取此扩展数据集并添加新列seq,该列提供每个id内特定事件的相对序列。这使我能够轻松地将每个事件与我下一步中紧接着的事件相匹配。

RunningTotalsCTE有诀窍使整个事情发挥作用:对于每个事件,它计算每个changeid值的运行总计。因此,此运行总计rangesActive应该给出每个事件日期的有效日期范围数。这允许我考虑重叠的日期范围。例如,如果您从RunningTotalsCTE id = 3中选择所有记录,则会得到以下内容:

seq  id  date                     change  rangesActive
1    3   2012-01-01 00:00:00.000  1       1
2    3   2013-01-01 00:00:00.000  1       2
3    3   2013-02-28 00:00:00.000  -1      1
4    3   2013-07-01 00:00:00.000  1       2
5    3   2014-01-01 00:00:00.000  -1      1
6    3   2014-06-01 00:00:00.000  -1      0

最后,GapsCTE通过查找rangesActive = 0的记录来识别所有差距,排除每个id中的最后一个事件。差距的大小是这种记录的事件日期与其后的记录的事件日期之间的差异。最后一步是从最终的CTE中选择唯一的id,其间隙大小为20天或更长。

我认为这会做你想要的,尽管如我所说,我不确定它将如何针对非常大的数据集执行。如果您对此如何运作有任何具体问题,请发表评论。

答案 1 :(得分:2)

使用您的小提琴架构,试试这个:

;WITH naivegaps AS
(
  SELECT ROW_NUMBER() OVER (ORDER BY id, startdate, MAX(dr1.enddate)) AS rn,
    dr1.Id, dr1.startdate, MAX(dr1.enddate) as enddate    
  FROM dateranges dr1
  GROUP BY dr1.Id, dr1.startdate
)
SELECT n1.id, n1.enddate as gap_start, n2.startdate AS gap_end, 
  datediff(dd, n1.enddate, n2.startdate) as gap_width, n3.*
FROM naivegaps n1
  CROSS APPLY
  (
    SELECT TOP 1 nx.id, nx.startdate
    FROM naivegaps nx
    WHERE n1.id = nx.id AND nx.rn > n1.rn
    ORDER BY nx.startdate
  ) n2
  OUTER APPLY
  (
    SELECT TOP 1 nx.id, nx.enddate
    FROM naivegaps nx
    WHERE n1.id = nx.id AND nx.rn < n1.rn
    ORDER BY nx.enddate DESC
  ) n3
WHERE datediff(dd, n1.enddate, n2.startdate) >= 20 AND (n3.enddate <= n1.enddate OR n3.enddate IS NULL)

顶部的CTE为下列检查适当地命令所有内容,并添加行号以便于订购检查。 CROSS APPLY找到序列结尾与下一个开头之间的所有间隙。 OUTER APPLY检查完全围绕相关差距的范围(不会在CROSS APPLY中对其进行适当排序)

编辑:我将此解决方案的执行计划与Joe Farrell提​​供的递归CTE解决方案进行了比较。他们的计划明显不同,但估计的效率非常接近(我的计划稍好一些,约为4%)。这可能会也可能不会转化为大型数据集的实际性能;我鼓励您测试这两种方法,并使用在您的方案中效果最佳的方法。

答案 2 :(得分:-1)

我重读了你的帖子并稍微编辑了这个回复,这应该更接近你真正想要的东西:

您可以使用min,max,group by和date diff。

执行此操作
Select T.Id, T.MinStartDate, T.MaxEndDate
FROM DateTable D
JOIN(SELECT Id, MIN(StartDate) AS  MinStartDate, MAX(EndDate) AS MaxEndDate, DATEDIFF(day, 
MIN(StartDate), MAX(EndDate)) AS Duration
FROM DateTable
GROUP BY Id) AS T ON T.id = D.id AND T.Duration > 20

有关datediff的更多信息:http://msdn.microsoft.com/en-us/library/ms189794.aspx