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:在由开始和结束定义的重叠日期范围内查找孤岛的主要内容,如果未能解决你的问题,请参考以下文章