【问题标题】:Find Id's with start/end date range coverage caps greater than 20 days查找开始/结束日期范围覆盖上限大于 20 天的 ID
【发布时间】:2014-06-06 20:37:28
【问题描述】:

我需要找到在第一个 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 应视为从 01/01/2012 到 06/01/2014 的一个范围,没有任何间隔。应忽略结束日期 02/28/2013 和开始日期 07/01/2013 之间的差距,因为从 01/01/2012 到 01/01/2014 的范围涵盖了差距。

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

【问题讨论】:

  • 您能写出您希望输出的样子吗?
  • 我只需要选择 Id 2,因为有超过 20 天的未覆盖间隙。

标签: sql sql-server-2008 date


【解决方案1】:

使用你的小提琴模式,试试这个:

;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%)。这可能会或可能不会转化为大型数据集的实际性能;我鼓励您测试这两种方法并使用最适合您的场景的方法。

【讨论】:

  • 谢谢。它的运行速度比光标快 4 倍。
  • 你能检查一下sqlfiddle.com/#!3/5bb1a/1/0吗?它不会选择从 2013-12-31 到 2014-03-01 的 id 4
【解决方案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 具有使整个事情起作用的技巧:对于每个事件,它计算每个 id 内的 change 值的运行总数。因此,该运行总数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 中选择唯一的 ids,其中差距大小为 20 天或更长时间。

我认为这将满足您的需求,但正如我所说,我不确定它在处理非常大的数据集时的表现如何。如果您对它的工作原理有任何具体问题,请发表评论。

【讨论】:

  • 递归 CTE 在这里很危险。我尝试针对中等数据集(600 行左右)对此进行测试并得到“消息 530,级别 16,状态 1,第 3 行语句终止。在语句完成之前,最大递归 100 已用尽。”
  • 好点。递归深度取决于每个id 的日期范围数,因此根据数据集的外观,它可能会或可能不会有问题,但我已经用大型数据集对其进行了测试,因为我有一点更多的时间,性能似乎很差。您的解决方案看起来好多了。
  • 用临时表替换 [SequencedEventsCTE] 有很大的不同。它现在可以在几秒钟内运行。谢谢。
【解决方案3】:

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

您可以使用最小值、最大值、分组依据和日期差异来做到这一点。

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

【讨论】:

  • 它没有任何帮助。
  • 我只需要选择 Id 2,因为有超过 20 天的未覆盖间隙。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-17
  • 1970-01-01
  • 1970-01-01
  • 2017-03-31
  • 2016-06-01
相关资源
最近更新 更多