在SQL中展平日期间隔

时间:2016-10-11 08:55:49

标签: sql oracle oracle11g

我有一个数据库表,其中有三列对此问题至关重要:

  • 将行组合在一起的组ID
  • 开始日期
  • 结束日期

我想从此表中创建一个视图,以便具有相同分组ID的重叠日期间隔被展平。

不重叠的日期间隔不得展平。

示例:

Group ID       Start         End
   1        2016-01-01   2017-12-31
   1        2016-06-01   2020-01-01
   1        2022-08-31   2030-12-31
   2        2010-03-01   2017-01-01
   2        2012-01-01   2013-12-31
   3        2001-01-01   9999-13-31

... ...变为

Group ID       Start         End
   1        2016-01-01   2020-01-01
   1        2022-08-31   2030-12-31
   2        2010-03-01   2017-01-01
   3        2001-01-01   9999-12-31

重叠的间隔可能以任何方式完成,完全被其他间隔包围,或者它们可能交错,或者它们甚至可能具有相同的开始和/或结束日期。

很少类似的ID。通常(> 95%)只有一行具有特定的组ID。大约有一千个ID出现在两行中;少数几个存在于三行中的ID;没有四行或更多行。

但我需要做好准备,可能会显示存在于四行或更多行中的组ID。

如何编写一个SQL语句来创建一个显示以这种方式展平的表的视图?

请注意,每一行也都有唯一的ID。这不需要以任何方式保留,但如果它在编写SQL时有帮助,我会通知您。

4 个答案:

答案 0 :(得分:2)

首先,找出不是重叠序列延续的区间:

select * 
from dateclap d1
where not exists(
    select * 
    from dateclap d2 
    where d2.group_id=d1.group_id and 
        d2.end_date >= d1.start_date and 
        (d2.start_date < d1.start_date or 
        (d1.start_date=d2.start_date and d2.r_id<d1.r_id)))

最后一行区分从相同日期/时间开始的间隔,按唯一记录ID(r_id)排序。

然后,对于每个这样的记录,我们可以通过connect_by_root r_id区分钳位组来获得记录的分层选择。之后,我们需要的是获得钳位组的最小值/最大值(connect_by_root r_id是组中父记录的id):

select group_id, min(start_date) as start_date, max(end_date) as end_date
from dateclap d1
start with not exists(
    select * 
    from dateclap d2 
    where d2.group_id=d1.group_id and 
        d2.end_date >= d1.start_date and 
        (d2.start_date < d1.start_date or 
        (d1.start_date=d2.start_date and d2.r_id<d1.r_id)))
connect by nocycle
    prior group_id=group_id and 
    start_date between prior start_date and prior end_date
group by group_id, connect_by_root r_id

注意这里的nocycle - 这是一个避免异常的肮脏技巧,因为连接很弱并且实际上试图将记录连接到自身。你可以在“连接”之后改进条件,类似于“存在”条件,以避免使用nocycle。

P.S。表是为这样的测试创建的:

CREATE TABLE "ANIKIN"."DATECLAP" 
(   
    "R_ID" NUMBER, 
    "GROUP_ID" NUMBER, 
    "START_DATE" DATE, 
    "END_DATE" DATE
) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
TABLESPACE "ANIKIN" ;

r_id和相应的seuqence / triggers的唯一键(或可能是主键)不是特定于测试的东西,只需用唯一值填充r_id。

答案 1 :(得分:1)

创建2个函数,返回特定元素的展平开始日期和结束日期:

CREATE OR REPLACE FUNCTION getMinStartDate
(
  p_group_id IN NUMBER,
  p_start    IN DATE
)
RETURN DATE AS
  v_result DATE;
BEGIN
  SELECT MIN(start_date)
    INTO v_result
    FROM my_data
   WHERE group_id = p_group_id
     AND start_date <= p_start
     AND end_date >= p_start;
  RETURN v_result;
END getMinStartDate;

CREATE OR REPLACE FUNCTION getMaxEndDate
(
  p_group_id IN NUMBER,
  p_end      IN DATE
)
RETURN DATE AS
  v_result DATE;
BEGIN
  SELECT MAX(end_date)
    INTO v_result
    FROM my_data
   WHERE group_id = p_group_id
     AND start_date <= p_end
     AND end_date >= p_end;
  RETURN v_result;
END getMaxEndDate;

然后,对于每个元素,您的视图应返回这些扁平日期 当然,DISTINCT因为各种元素可能导致相同的日期:

SELECT DISTINCT
       group_id,
       getMinStartDate(group_id, start_date) AS start_date,
       getMaxEndDate(group_id, end_date) AS end_date
FROM   my_data;

答案 2 :(得分:1)

   select t1.group_id, least(min(t1.start_date),  min(t2.start_date)),  greatest(max(t1.start_date), max(t2.end_date)) from test_interval t1, test_interval t2 
   where (t1.start_date, t1.end_date) overlaps (t2.start_date, t2.end_date) 
      and t1.rowid <> t2.rowid 
      and t1.group_id = t2.group_id group by t1.group_id;

此类查询为我生成重叠间隔列表。 OVERLAPS是一个无证的运营商。我只是想知道当我们得到两对重叠而不是彼此重叠的区间时,这是否会返回错误的结果。 在我使用rowid的地方,你可以使用你唯一的行标识符

答案 3 :(得分:1)

输入数据显示最后一行的结束日期为9999-13-31。这应该得到纠正。

话虽如此,最好选择一个不完全是9999-12-31的结束日期。在许多问题中,人们需要在表中的所有日期添加一天,或几周或其他任何东西;但如果试图添加到9999-12-31,那将失败。我更喜欢8999-12-31;对于大多数计算而言,一千年应该足够了。 {:-)在我为查询创建的测试数据中,我使用了这个约定。 (虽然解决方案很容易适应9999-12-31。)

使用日期时间间隔时,请记住纯日期表示一天开始时的午夜。所以2016年的“结束日期”为2017-01-01(当天午夜),而2017年的“开始日期”也是2017-01-01。因此,表格应该具有相同的结束日期和开始日期,以便紧接着彼此相继 - 并且它们应该融合在一起形成一个区间。但是,2016-08-31结束的时间间隔和2016-09-01开始的时间间隔不应融合在一起;它们相隔一整天(特别是2016-08-31的那一天不包括在任何一个区间内)。

OP没有具体说明如何解释结束日期。我假设它们如最后一段所述;否则解决方案可以很容易地进行调整(但是需要先添加1到结束日期,然后在结尾处减去1 - 这正是9999-12-31不是“未知”的好占位符的情况之一。 )

<强>解决方案

with m as
        (
         select group_id, start_date,
                   max(end_date) over (partition by group_id order by start_date 
                             rows between unbounded preceding and 1 preceding) as m_time
         from inputs   -- "inputs" is the name of the base table
         union all
         select group_id, NULL, max(end_date) from inputs group by group_id
        ),
     n as
        (
         select group_id, start_date, m_time 
         from m 
         where start_date > m_time or start_date is null or m_time is null
        ),
     f as
        (
         select group_id, start_date,
            lead(m_time) over (partition by group_id order by start_date) as end_date
         from n
        )
select * from f where start_date is not null
;

输出(提供数据):

  GROUP_ID START_DATE END_DATE 
---------- ---------- ----------
         1 2016-01-01 2020-01-01
         1 2022-08-31 2030-12-31
         2 2010-03-01 2017-01-01
         3 2001-01-01 8999-12-31