具有开始和结束日期的差距和孤岛(有效期)

Posted

技术标签:

【中文标题】具有开始和结束日期的差距和孤岛(有效期)【英文标题】:Gaps and Islands with start en end date (ValidPeriod) 【发布时间】:2016-11-16 15:28:06 【问题描述】:

我已搜索但找不到以下问题的解决方案。

我有几个价目表有几百万行,我发现了许多可以汇总到一行的示例,因为该组的开始日期和结束日期是连续的(结束日期:20151231 下一个开始日期:20160101)

但我也发现了许多差距,这意味着使用 min() 和 max() 函数的直接方法不适用,因为可能的差距将被忽略。

以下包含一个带有示例记录的#Price 表和一个带有我要拍摄的结果的#Target 表:

谢谢。

我对间隔的定义是两条连续记录之间的间隔超过 1 天。

if object_id('tempdb..#Prices', 'table') is not null
    drop table #Prices
;

create table #Prices (
    Product         varchar(50) not null
  , Value           decimal(18,5) not null
  , ValidFrom       date not null
  , ValidTo         date null
)

insert into #Prices
(
    Product
  , Value
  , ValidFrom   
  , ValidTo    
)
select
    Product          = 'Island A'
  , Value            = 10.10
  , ValidFrom        = '20140101'
  , ValidTo          = '20140606'
union all
select
    Product          = 'Island A'
  , Value            = 10.10
  , ValidFrom        = '20140607'
  , ValidTo          = '20141010'
union all
select
    Product          = 'Island A'
  , Value            = 10.11
  , ValidFrom        = '20141011'
  , ValidTo          = '20141231'
union all
select
    Product          = 'Island A'
  , Value            = 11.10
  , ValidFrom        = '20150101'
  , ValidTo          = '20151231'
union all
select
    Product          = 'Island A'
  , Value            = 10.10
  , ValidFrom        = '20160101'
  , ValidTo          = null
union all
select
    Product          = 'Gap B'
  , Value            = 20.10
  , ValidFrom        = '20140101'
  , ValidTo          = '20140606'
union all
select
    Product          = 'Gap B'
  , Value            = 20.10
  , ValidFrom        = '20140607'
  , ValidTo          = '20141010'
union all
select
    Product          = 'Gap B'
  , Value            = 20.10
  , ValidFrom        = '20150101'
  , ValidTo          = '20151231'
union all
select
    Product          = 'Gap B'
  , Value            = 20.10
  , ValidFrom        = '20160101'
  , ValidTo          = null

select * 
from #Prices as P
order by P.Product, P.ValidFrom
;


if object_id('tempdb..#Target', 'table') is not null
    drop table #Target
;

create table #Target (
    Product         varchar(50) not null
  , Value           decimal(18,5) not null
  , ValidFrom       date not null
  , ValidTo         date null
)

insert into #Target
(
    Product
  , Value
  , ValidFrom   
  , ValidTo    
)
select
    Product          = 'Island A'
  , Value            = 10.10
  , ValidFrom        = '20140101'
  , ValidTo          = '20141010'
union all
select
    Product          = 'Island A'
  , Value            = 10.11
  , ValidFrom        = '20141011'
  , ValidTo          = '20141231'
union all
select
    Product          = 'Island A'
  , Value            = 11.10
  , ValidFrom        = '20150101'
  , ValidTo          = '20151231'
union all
select
    Product          = 'Island A'
  , Value            = 10.10
  , ValidFrom        = '20160101'
  , ValidTo          = null
union all
select
    Product          = 'Gap B'
  , Value            = 20.10
  , ValidFrom        = '20140101'
  , ValidTo          = '20141010'
union all
select
    Product          = 'Gap B'
  , Value            = 20.10
  , ValidFrom        = '20150101'
  , ValidTo          = null

select * 
from #Target as P
order by P.Product, P.ValidFrom
;

编辑 我希望编辑是您问题的答案。可以通过取 min(ValidFrom) 和 max(ValidTo) 来聚合连续的记录(记录之间最多 1 天)。问题在于差距,这些将被忽略。产品“差距 B”的结果将是一条记录。 即使日期在 Gap 期间,任何使用 Date 的记录都将获得值 20.10。

Gap B    |    20.10    |    20140101    |     null

因此我需要 2 条记录,因此表上的所有连接都将产生正确的值,而在 Gap 期间没有值

Gap B    |    20.10    |    20140101    |     20141010
Gap B    |    20.10    |    20151231    |     null

【问题讨论】:

请仅显示您的表格数据示例,以及为什么这表明您存在间隙和孤岛问题。 @TimBiegeleisen,我对原始问题做了一点补充。我希望这很清楚。 Grzt 亚历山大 【参考方案1】:

这是一个使用递归 cte 的不同解决方案,我认为与 Jon 的相比,它更容易理解。在这样的数据量上,它的效率也更高,尽管您需要自己测试更大数据集的性能:

;with rownum
as
(
    select row_number() over (order by Product, ValidFrom) as rn
            ,Product
            ,Value
            ,ValidFrom
            ,ValidTo
    from #Prices
)
,cte
as
(
    select rn
            ,Product
            ,Value
            ,ValidFrom
            ,ValidFrom as ValidFrom2
            ,ValidTo
    from rownum
    where rn = 1

    union all

    select r.rn
            ,r.Product
            ,r.Value

            ,r.ValidFrom
            ,case when c.Product = r.Product
                    then case when dateadd(d,1,c.ValidTo) = r.ValidFrom
                            then c.ValidFrom
                            else r.ValidFrom
                            end
                    else r.ValidFrom
                    end as ValidFrom2

            ,isnull(r.ValidTo,'29990101') as ValidTo
    from rownum r
        inner join cte c
            on(r.rn = c.rn+1)
)
select Product
        ,Value
        ,ValidFrom2 as ValidFrom
        ,nullif(max(ValidTo),'29990101') as ValidTo
from cte
group by Product
        ,Value
        ,ValidFrom2
order by Product
        ,ValidFrom2;

【讨论】:

不确定我是否同意“更容易理解”,但我们都有自己的偏好和教条。在大数据上,递归 ctes 可以是一条狗。例如:使用 r-cte 构建 200K 层次结构可能需要将近 5 分钟,而我的其他方法需要 7 秒(完成范围键) @iamdave,太棒了!正是医生吩咐的。您交付速度的额外积分。谢谢。 grtz 亚历山大 @JohnCappelletti 同意,因此需要进行测试的警告。在较小的数据集上,我认为我的解决方案会表现得更好,尽管您的解决方案缺少递归肯定会在更大的数据集上表现得更好。 @iamdave 只是为了好玩,我测试了你和我的。根据提供的数据,您的平均速度快了 15 毫秒(每次运行 5 次) @JohnCappelletti 每一点都有帮助!【参考方案2】:

必须为 NULL ValidTo 编写一些逻辑

;with cte0(N)   as (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N))
     ,cte1(R,D) as (Select Row_Number() over (Order By (Select Null))
                          ,DateAdd(DD,Row_Number() over (Order By (Select Null)) -1,(Select min(ValidFrom) From  #Prices)) 
                     From  cte0 N1, cte0 N2, cte0 N3, cte0 N4) 
Select Product
      ,Value    
      ,ValidFrom = Min(ValidFrom)
      ,ValidTo   = nullif(max(isnull(ValidTo,'2099-12-31')),'2099-12-31')
 From (
         Select *
               ,Island = R - Row_Number() over (Partition By Product,Value Order by ValidFrom)
          From  #Prices  A
          Join  cte1     B on D Between ValidFrom and IsNull(ValidTo,'2099-12-31')
      ) A
 Group By Product,Value,Island
 Order By 1 Desc,3

返回

Product     Value       ValidFrom   ValidTo
Island A    10.10000    2014-01-01  2014-10-10
Island A    10.11000    2014-10-11  2014-12-31
Island A    11.10000    2015-01-01  2015-12-31
Island A    10.10000    2016-01-01  NULL
Gap B       20.10000    2014-01-01  2014-10-10
Gap B       20.10000    2015-01-01  NULL

【讨论】:

我疯了还是这条线case when Max(IsNull(ValidTo,'2099-12-31'))='2099-12-31' then null else Max(IsNull(ValidTo,'2099-12-31')) endnullif(max(isnull(ValidTo,'2099-12-31')),'2099-12-31')一样? @JohnCappelletti,比“iamdave”更令人印象深刻。但是这个解决方案需要对工作原理进行一些调查。我很快就会回来找你。 gtz亚历山大 @Alexander 老实说,无论哪种解决方案最适合您……性能和/或可支持性 @Alexander 不管怎样,我仍然给 iamdave 一个加分项 @JohnCappelletti,这样就可以了……谢谢!刚刚将此帖子标记为解决方案。你真是个聪明人。 grtz 亚历山大

以上是关于具有开始和结束日期的差距和孤岛(有效期)的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server:寻找就业差距——孤岛和差距问题

PL/SQL:在由开始和结束定义的重叠日期范围内查找孤岛

将具有给定时间的 NSDate 对象转换为具有相同时间和当前日期的对象的最有效方法

差距和孤岛问题 - 查询不适用于所有时期

过滤掉基于数据的有效数据和期限数据

令人费解的差距和孤岛 ORACLE SQL