PL/SQL: 如何将具有连续时间范围的多行转换为覆盖整个时间范围的一行?

PL/SQL: How to convert multiple rows with continuous time frames into one single row convering the whole time frame?

假设我有以下数据库 table:

id      |     from        |     to
 1      |    01-JAN-2015  |    03-MAR-2015
 1      |    04-MAR-2015  |    31-AUG-2015
 1      |    01-SEP-2015  |    31-DEC-2015
 2      |    01-JAN-2015  |    30-JUN-2015
 2      |    01-NOV-2015  |    31-DEC-2015

并且我想将时间上连续的具有相同id的记录汇总成一行覆盖整个时间范围,如下:

id      |     from        |     to
 1      |    01-JAN-2015  |    31-DEC-2015
 2      |    01-JAN-2015  |    30-JUN-2015
 2      |    01-NOV-2015  |    31-DEC-2015

因此,由于时间范围是连续的并且它们之间没有间隙,所以 id 1 的 3 行可以转换为具有最小起始日期和最大截止日期的 1 行。 id 2 的 2 行将保持不变,因为时间范围不连续。

我正在考虑通过游标使用循环来执行此操作,但我可能会使事情复杂化。 有更好的主意吗?也许只有 SQL 个查询?

您可以在具有一些分析和聚合函数的阶段执行此操作:

with t1(id, from_dt, to_dt) as (
  select 1, to_date('01-JAN-2015', 'dd-mon-rrrr'), to_date('03-MAR-2015', 'dd-mon-rrrr') from dual union all
  select 1, to_date('04-MAR-2015', 'dd-mon-rrrr'), to_date('31-AUG-2015', 'dd-mon-rrrr') from dual union all
  select 1, to_date('01-SEP-2015', 'dd-mon-rrrr'), to_date('31-DEC-2015', 'dd-mon-rrrr') from dual union all
  select 2, to_date('01-JAN-2015', 'dd-mon-rrrr'), to_date('30-JUN-2015', 'dd-mon-rrrr') from dual union all
  select 2, to_date('01-NOV-2015', 'dd-mon-rrrr'), to_date('31-DEC-2015', 'dd-mon-rrrr') from dual
), t2 as (
  select id
       , from_dt
       , to_dt
       , from_dt-lag(to_dt,1,from_dt-1) over (partition by id order by to_dt) dst
       , row_number() over (partition by id order by to_dt) rn
    from t1
), t3 as (
  select id
       , from_dt
       , to_dt
       , sum(dst) over (partition by id order by rn) - rn grp
    from t2
)
select id
     , min(from_dt) from_dt
     , max(to_dt) to_dt
  from t3
 group by id, grp;

第一阶段 T1 只是重新创建您的数据。在 T2 中,我从 from_dt 中减去 to_dt 的滞后以找到连续记录之间的距离 (dst) 并为每个记录 (rn) 生成 row_number。在 T3 中,我从 dst 的 运行 总和中减去 rn 以生成组 ID (grp)。最后在输出阶段,我将 from_dt 和 to_dt 的最小值和最大值分别按 ID 和 grp 列分组。

您可以使用分层查询来实现,如下所示:

select id, min(root_dt_from) dt_from, dt_to
  from (select id, dt_from, dt_to, level, connect_by_isleaf, connect_by_root(dt_from) root_dt_from
          from t
          where connect_by_isleaf = 1
        connect by prior id = id and prior (dt_to + 1) = dt_from
       )
 group by id, dt_to;

示例执行:

SQL> with t as (
  2  select 1 id, to_date('01-JAN-2015', 'DD-MON-YYYY') dt_from, to_date('03-MAR-2015', 'DD-MON-YYYY') dt_to from dual union all
  3  select 1 id, to_date('04-MAR-2015', 'DD-MON-YYYY') dt_from, to_date('31-AUG-2015', 'DD-MON-YYYY') dt_to from dual union all
  4  select 1 id, to_date('01-SEP-2015', 'DD-MON-YYYY') dt_from, to_date('31-DEC-2015', 'DD-MON-YYYY') dt_to from dual union all
  5  select 2 id, to_date('01-JAN-2015', 'DD-MON-YYYY') dt_from, to_date('30-JUN-2015', 'DD-MON-YYYY') dt_to from dual union all
  6  select 2 id, to_date('01-NOV-2015', 'DD-MON-YYYY') dt_from, to_date('31-DEC-2015', 'DD-MON-YYYY') dt_to from dual
  7  ) -- end of sample data
  8  select id, min(root_dt_from) dt_from, dt_to
  9    from (select id, dt_from, dt_to, level, connect_by_isleaf, connect_by_root(dt_from) root_dt_from
 10            from t
 11            where connect_by_isleaf = 1
 12          connect by prior id = id and prior (dt_to + 1) = dt_from
 13         )
 14   group by id, dt_to;

        ID DT_FROM     DT_TO
---------- ----------- -----------
         1 01-JAN-2015 31-DEC-2015
         2 01-NOV-2015 31-DEC-2015
         2 01-JAN-2015 30-JUN-2015

You can try here some analytical functions which can really simplify the scenario. Hope this below snippet helps. Let me know for any issues.

SELECT B.ID,
  MIN(B.FRM_DT) FRM_DT,
  MAX(B.TO_DT) TO_DT
FROM
  (SELECT A.ID,
    A.FRM_DT,
    A.TO_DT,
    NVL(LAG(A.TO_DT+1) OVER(PARTITION BY A.ID ORDER BY A.TO_DT),A.FRM_DT) nxt_dt,
    CASE
      WHEN NULLIF(A.FRM_DT,NVL(LAG(A.TO_DT+1) OVER(PARTITION BY A.ID ORDER BY A.TO_DT),A.FRM_DT)) IS NULL
      THEN 'True'
      ELSE 'False'
    END COND
  FROM
    (SELECT 1 AS ID,
      TO_DATE('01/01/2015') FRM_DT,
      TO_DATE('03/03/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 1 AS ID,
      TO_DATE('03/04/2015') FRM_DT,
      TO_DATE('07/31/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 1 AS ID,
      TO_DATE('08/01/2015') FRM_DT,
      TO_DATE('12/31/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 2 AS ID,
      TO_DATE('01/01/2015') FRM_DT,
      TO_DATE('06/30/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 2 AS ID,
      TO_DATE('11/01/2015') FRM_DT,
      TO_DATE('12/31/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 3 AS ID,
      TO_DATE('01/01/2015') FRM_DT,
      TO_DATE('03/14/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 3 AS ID,
      TO_DATE('03/15/2015') FRM_DT,
      TO_DATE('11/30/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 3 AS ID,
      TO_DATE('12/01/2015') FRM_DT,
      TO_DATE('12/31/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 4 AS ID,
      TO_DATE('02/01/2015') FRM_DT,
      TO_DATE('05/30/2015') TO_DT
    FROM DUAL
    UNION
    SELECT 4 AS ID,
      TO_DATE('06/01/2015') FRM_DT,
      TO_DATE('12/31/2015') TO_DT
    FROM DUAL
    )A
  )B
GROUP BY B.ID,
  B.COND; 

-----------------------------------OUTPUT------------------------------------------

ID   FRM_DT            TO_DT
4   02/01/2015  05/30/2015
4   06/01/2015  12/31/2015
1   01/01/2015  12/31/2015
2   01/01/2015  06/30/2015
2   11/01/2015  12/31/2015
3   01/01/2015  12/31/2015
-----------------------------------OUTPUT------------------------------------------