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

Posted

技术标签:

【中文标题】PL/SQL:在由开始和结束定义的重叠日期范围内查找孤岛【英文标题】:PL/SQL: Finding islands in overlapping date ranges defined by a start and an end 【发布时间】:2016-08-08 12:51:59 【问题描述】:

我有一个相当大的数据集,其中包含人员及其保险的开始和结束日期。每个人可以有多个日期重叠的记录。我需要为每个人找到每个“岛屿”的起点和终点。

例如:

    SKP_PERSON  DATE_INSURANCE_START    DATE_INSURANCE_END   SKP_INSURANCE
1   1           7.11.2015               1.1.3000             1
2   1           7.11.2015               1.1.3000             2
3   2           10.4.2015               1.8.2016 23:59:59    3
4   3           28.3.2016               1.1.3000             4
5   4           5.12.2015               31.12.2015 23:59:59  5
6   4           5.12.2015               1.5.2016 23:59:59    6
7   4           1.2.2016                1.5.2016 23:59:59    7
8   5           15.1.2016               2.3.2016 23:59:59    8
9   5           15.3.2016               2.6.2016 23:59:59    9

我需要的结果是这样的:

    SKP_PERSON  DATE_INSURANCE_START    DATE_INSURANCE_END   
1   1           7.11.2015               1.1.3000             
2   2           10.4.2015               1.8.2016 23:59:59    
3   3           28.3.2016               1.1.3000             
4   4           5.12.2015               1.5.2016 23:59:59    
5   5           15.1.2016               2.3.2016 23:59:59    
6   5           15.3.2016               2.6.2016 23:59:59    

我设法通过将所有可能的日期(从 min(start) 到 max(end))加入每个人并找到每一天的滞后和领先值来找到解决方案 - 但记录和日期范围太多太大了,所以需要很长时间。有没有更有效的使用 PL/SQL 的解决方案?

编辑: 我试过的查询(简化):

WITH table1 AS (
SELECT d.dtime_day, COUNT(i.dkp_insurance), i.skp_person 
FROM date d --a date table, contains a record for every day
JOIN insurance i ON d.dtime_day BETWEEN i.DATE_INSURANCE_START AND i.DATE_INSURANCE_END    
GROUP BY d.dtime_day, i.skp_person
)
SELECT * FROM 
(
SELECT distinct skp_person, 
CASE WHEN LAG(dtime_day) OVER (PARTITION BY skp_person ORDER BY dtime_day) <> dtime_day -1 THEN dtime_day END AS start,
CASE WHEN LEAD(dtime_day) OVER (PARTITION BY skp_person ORDER BY dtime_day) <> dtime_day +1 THEN dtime_day END AS end
FROM table1 t1)
WHERE start IS NOT NULL OR end IS NOT NULL 
ORDER BY skp_person
; 

【问题讨论】:

你能提供你已经尝试过的SQL吗?另外,这些表上是否有任何索引?谢谢。 我不太确定我正在使用的数据仓库中的表是如何建立索引的——虽然 SKP_INSURANCE 是一个主键。我将在编辑中添加代码。 几个月前我回答了一个类似的问题,请看一下。从您编写的代码中,我看到您可以处理 SQL,您所需要的只是算法的想法(解决问题的方法);如果您认为可以使用我的解决方案,但需要进一步的帮助,请说出来。祝你好运! ***.com/questions/36387048/… 查看您的样本数据,您可以在以不同方式处理开放区间 (end=1.1.3000) 时显着提高性能。 @mathguy:我将不得不对其进行正确测试,但看起来这可能正是我所需要的——我什至不需要更改任何内容,除了列名。非常感谢! 【参考方案1】:

我针对这种情况调整了我的旧解决方案(请参阅对原始问题的评论)。需要愚蠢的 +1/86400(增加一秒)来处理表格中奇怪的结束日期/时间。

with
     inputs ( skp_person, date_insurance_start, date_insurance_end ) as (
       select 1, to_date('7.11.2015', 'dd.mm.yyyy'), to_date('1.1.3000'           , 'dd.mm.yyyy')             from dual union all
       select 1, to_date('7.11.2015', 'dd.mm.yyyy'), to_date('1.1.3000'           , 'dd.mm.yyyy')             from dual union all
       select 2, to_date('10.4.2015', 'dd.mm.yyyy'), to_date('1.8.2016 23:59:59'  , 'dd.mm.yyyy  hh24:mi:ss') from dual union all
       select 3, to_date('28.3.2016', 'dd.mm.yyyy'), to_date('1.1.3000'           , 'dd.mm.yyyy')             from dual union all
       select 4, to_date('5.12.2015', 'dd.mm.yyyy'), to_date('31.12.2015 23:59:59', 'dd.mm.yyyy hh24:mi:ss')  from dual union all
       select 4, to_date('5.12.2015', 'dd.mm.yyyy'), to_date('1.5.2016 23:59:59'  , 'dd.mm.yyyy hh24:mi:ss')  from dual union all
       select 4, to_date('1.2.2016' , 'dd.mm.yyyy'), to_date('1.5.2016 23:59:59'  , 'dd.mm.yyyy hh24:mi:ss')  from dual union all
       select 5, to_date('15.1.2016', 'dd.mm.yyyy'), to_date('2.3.2016 23:59:59'  , 'dd.mm.yyyy hh24:mi:ss')  from dual union all
       select 5, to_date('15.3.2016', 'dd.mm.yyyy'), to_date('2.6.2016 23:59:59'  , 'dd.mm.yyyy hh24:mi:ss')  from dual
     ),
     m ( skp_person, date_insurance_start, m_date ) as (
       select  skp_person, date_insurance_start, 
               max(date_insurance_end + 1/86400)
               over (partition by skp_person order by date_insurance_start
                    rows between unbounded preceding and 1 preceding)
         from  inputs
       union all
       select  skp_person, null, max(date_insurance_end + 1/86400)
         from  inputs
         group by skp_person
     ),
     f ( skp_person, date_insurance_start, e_date ) as (
       select  skp_person, date_insurance_start,
               lead(m_date) over 
               (partition by skp_person order by date_insurance_start)
       from    m
       where   date_insurance_start > m_date 
          or   date_insurance_start is null   or m_date is null
     )
select skp_person, date_insurance_start, e_date - 1/86400 as date_insurance_end
from f where date_insurance_start is not null
;

输出:(使用我的 NLS_DATE_FORMAT 设置)

SKP_PERSON DATE_INSURANCE_STAR DATE_INSURANCE_END
---------- ------------------- -------------------
         1 07.11.2015 00:00:00 01.01.3000 00:00:00
         2 10.04.2015 00:00:00 01.08.2016 23:59:59
         3 28.03.2016 00:00:00 01.01.3000 00:00:00
         4 05.12.2015 00:00:00 01.05.2016 23:59:59
         5 15.01.2016 00:00:00 02.03.2016 23:59:59
         5 15.03.2016 00:00:00 02.06.2016 23:59:59

【讨论】:

非常感谢,结果看起来是正确的。 cmets 的解决方案几乎奏效了,只是有时无法连接以连续日期开始和结束的组。 是的 - 旧的解决方案需要适应奇怪的结束日期,这就是我在此处发布的解决方案中所做的。祝你好运!【参考方案2】:

这是一个想法:

使用lag() 或其他方法来确定岛屿的开始时间 在岛屿开始时构造一个为 1 的标志 运行累积和 重新聚合

生成的查询如下所示:

select skp_person,
       min(date_insurance_start) as date_insurance_start,
       min(date_insurance_end) as date_insurance_end
from (select t.*,
             sum(isIslandFlag) over (partition by skp_person order by date_insurance_start) as grp
      from (select t.*,
                   (case when exists (select 1
                                      from t t2
                                      where t2.skp_person = t.skp_person and
                                            t2.date_insurance_start between t.date_insurance_start and t.date_insurance_end
                                     )
                         then 0 else 1
                   end) as IsIslandFlag
            from t
           ) t
      ) t
group by skp_person, grp;

注意:此方法并非万无一失,但它适用于大多数实际情况。例如,如果您有多个在同一天开始的政策,则需要对其进行一些调整。

【讨论】:

我将不得不正确查看查询,但它无法正常工作。示例中的 SKP_PERSON 4 返回 5.12.2015 和 31.12.2015。无论如何,谢谢,如果没有别的,这是一个灵感。 您的值是存储为日期还是字符串?

以上是关于PL/SQL:在由开始和结束定义的重叠日期范围内查找孤岛的主要内容,如果未能解决你的问题,请参考以下文章

SQL 重叠日期范围

BigQuery 计算两个日期范围重叠

如何在 SQL 中选择重叠的日期范围

Oracle Native Dynamic SQL PL/SQL 语句没有开始和结束

如何使用开始和结束日期时间在司机的行程中查找重叠记录

Oracle SQL 选择具有开始和结束日期的行,如果某些重叠合并行