【问题标题】:How to merge/split multiple spans of dates into one timeline (Oracle 11g)?如何将多个日期范围合并/拆分为一个时间线(Oracle 11g)?
【发布时间】:2011-10-19 00:25:01
【问题描述】:

我已经为这个问题苦苦挣扎了几天,现在我正在向群众寻求帮助。

我的问题与此站点上的先前解决方案类似,但并不完全相同: PL/SQL Split, separate a date into new dates according to black out dates! 这个解决方案相当布尔(包括/排除),而我的问题涉及其中的一些以及合并。

虽然我认为我对 SQL+PL/SQL 有中级/高级的掌握……Oracle 分析功能显然让我大吃一惊。我一直在尝试阅读/学习,但时间不多了。

由于我不确定共享表名 (COTS)、业务线等的合法性,我将用一个模糊的场景/上下文来模仿我的问题。希望这能驱散律师精神。

关于问题: 我有一张包含客户活动历史记录的表格。客户可以来来去去,因此我们在此表中可能有多行(每个客户)。

CREATE TABLE activity AS
SELECT 1 AS cust_id,
       TO_DATE('01-JAN-2010') AS start_dt,
       TO_DATE('31-JUL-2010') AS end_dt,
       'EAST' AS region
FROM DUAL
UNION
SELECT 1 AS cust_id,
       TO_DATE('01-FEB-2011') AS start_dt,
       TO_DATE('31-DEC-2011') AS end_dt,
       'EAST' AS region
FROM DUAL;

我还有一个按跨度存储属性信息的表。一个客户可以同时拥有多种属性类型,并且每种类型在不同的时间跨度内多次出现。

CREATE TABLE attrib AS
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-JAN-2009') AS atb_start_dt,
       TO_DATE('31-JAN-2010') AS atb_end_dt,
       'LocalNuke' AS provider,
       1.80 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-MAR-2010') AS atb_start_dt,
       TO_DATE('31-MAR-2010') AS atb_end_dt,
       'CoalGuys' AS provider,
       1.60 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-JUN-2010') AS atb_start_dt,
       TO_DATE('30-SEP-2010') AS atb_end_dt,
       'LocalNuke' AS provider,
       1.70 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'POWER' AS atb_cd,
       TO_DATE('01-MAR-2011') AS atb_start_dt,
       TO_DATE('31-DEC-9999') AS atb_end_dt,
       'GeoHeat' AS provider,
       1.10 AS per_kwh,
       0 AS per_gal
FROM DUAL
UNION
SELECT 1 AS cust_id,
       'WATER' AS atb_cd,
       TO_DATE('01-MAR-2010') AS atb_start_dt,
       TO_DATE('31-DEC-9999') AS atb_end_dt,
       'GlacialGold' AS provider,
       0 AS per_kwh,
       0.60 AS per_gal
FROM DUAL;

数据异常是故意的,我试图在不与“现实世界”相关的情况下使这个场景尽可能真实。

结果应该将跨度限制在客户与这家虚构公司的活动中,并拆分所有重叠的日期以形成时间线。需要将数据元素合并在一起以进行报告。

视觉上:

Cust:
         |----------------------|             |------------------------|
Power:
|-------------|    |--|    |-------|               |---------------------->
Water:
                   |------------------------------------------------------>    
Expected Result:
         |----|----|--|----|----|             |----|-------------------|

解决方案应该可以扩展以包含其他属性。最后,我会将这些非规范化的信息放在一个表格中,这样我就可以在任何时间点报告客户的数据。例如,如果他们在某一天有活动、有电和有水;我应该可以导出当天的 per_kwh、per_gal 和活动数据。

示例输出(表格):

CUST_ID  FROM_DT      THRU_DT      REGION  POWER_PROVIDER  WATER_PROVIDER  PER_KWH  PER_GAL
-------  -----------  -----------  ------  --------------  --------------  -------  -------
1        01-JAN-2010  31-JAN-2010  EAST    LocalNuke                       1.80     0
1        01-FEB-2010  28-FEB-2010  EAST                                    0        0
1        01-MAR-2010  31-MAR-2010  EAST    CoalGuys        GlacialGold     1.60     0.60
1        01-APR-2010  31-MAY-2010  EAST                    GlacialGold     0        0.60
1        01-JUN-2010  31-JUL-2010  EAST    LocalNuke       GlacialGold     1.70     0.60
1        01-FEB-2011  28-FEB-2011  EAST                    GlacialGold     0        0.60
1        01-MAR-2011  31-DEC-2011  EAST    GeoHeat         GlacialGold     1.10     0.60

我在大约 2 年前写了一些东西(当时的需求类似于 Activity/Power),使用 2 个异步游标逐行(逐行)处理。

虽然性能很重要,但我尝试寻找直接/批量 sql 解决方案的最大原因是维护。我的原始解决方案的 if/else 光标嵌套已经很难遵循,并且会成倍恶化,至少还有 2 个“属性”跨度可以拆分。

如果你们能提供任何帮助,我将不胜感激。

【问题讨论】:

  • 你能发布你想要的查询的输出吗?视觉效果很有帮助,但我不确定我是否了解如何将视觉效果转换为某种表格查询结果。
  • 问题已修改以包含预期的表格输出 - @JustinCave 推荐

标签: oracle date


【解决方案1】:

这可能有效。它不会将相邻区域合并在一起,但它仍然可以完成工作。

WITH

  milestone AS
  (
    SELECT cust_id, start_dt     AS point_in_time FROM ACTIVITY
  UNION
    SELECT cust_id, atb_start_dt AS point_in_time FROM ATTRIB
  UNION
    SELECT cust_id, LEAST(end_dt,     TO_DATE('30-DEC-9999')) + 1 AS point_in_time FROM ACTIVITY
  UNION
    SELECT cust_id, LEAST(atb_end_dt, TO_DATE('30-DEC-9999')) + 1 AS point_in_time FROM ATTRIB
  )

SELECT
  milestone.cust_id                 AS cust_id,
  milestone.point_in_time           AS from_dt,
  LEAD(point_in_time)
    OVER (PARTITION BY milestone.cust_id ORDER BY milestone.point_in_time) - 1
                                    AS thru_dt,
  activity.region                   AS region,
  power_attrib.provider             AS power_provider,
  water_attrib.provider             AS water_provider,
  COALESCE(power_attrib.per_kwh, 0) AS per_kwh,
  COALESCE(water_attrib.per_gal, 0) AS per_gal
FROM
  MILESTONE

  LEFT OUTER JOIN ACTIVITY
    ON milestone.cust_id = activity.cust_id
       AND milestone.point_in_time BETWEEN activity.start_dt AND activity.end_dt

  LEFT OUTER JOIN ATTRIB power_attrib
    ON milestone.cust_id = power_attrib.cust_id
       AND power_attrib.atb_cd = 'POWER'
       AND milestone.point_in_time BETWEEN power_attrib.atb_start_dt AND power_attrib.atb_end_dt

  LEFT OUTER JOIN ATTRIB water_attrib
    ON milestone.cust_id = water_attrib.cust_id
       AND water_attrib.atb_cd = 'WATER'
       AND milestone.point_in_time BETWEEN water_attrib.atb_start_dt AND water_attrib.atb_end_dt

【讨论】:

  • 感谢您的宝贵时间!我需要一些时间来理解/剖析和应用您的反馈。我稍后会更新结果和(如果成功)奖励答案。
  • 这看起来很有希望。我已经为我的系统重新编写了它,并且不得不调整一些东西(不想要非活动行)。在我批准答案之前,我仍然想明天做更多的测试(又名,虽然我仍然希望能引起你的注意)。你的解决方案是如此直接(优雅)我想踢自己的胫骨!
  • @Brock:很高兴能帮上忙。 :) 前几天我不得不解决这个问题。
【解决方案2】:

这确实是一个非常棘手的问题,我希望您最终会得到一个大而混乱的查询。您遇到的核心问题是您需要为属性表中的间隙制造“伪”行。这是有问题的。

我对您的问题进行了简化,只是试图为 POWER 属性制造差距。我认为每个属性行前面都可以有一个间隙。想出了这个

SELECT  PS.cust_id
    ,   G.is_gap
    ,   DECODE( G.is_gap, 'Y', PS.prev_start, PS.atb_start_dt ) AS start_date
    ,   DECODE( G.is_gap, 'Y', PS.prev_end, PS.atb_end_dt ) AS end_date
    ,   DECODE( G.is_gap, 'Y', NULL, PS.provider ) AS provider
    ,   DECODE( G.is_gap, 'Y', NULL, PS.per_kwh ) AS per_kwh
    ,   DECODE( G.is_gap, 'Y', NULL, PS.per_gal ) AS per_gal
FROM
    (   SELECT  P.cust_id
            ,   P.atb_start_dt
            ,   P.atb_end_dt
            ,   P.provider
            ,   P.per_kwh
            ,   P.per_gal
            ,   P.atb_start_dt - 1      AS prev_end
            ,   NVL( MAX( P.atb_end_dt ) OVER ( ORDER BY P.atb_end_dt
                        ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING ) + 1
                   , '01-JAN-1900' )    AS prev_start
        FROM    attrib      P
        WHERE   P.atb_cd    = 'POWER'
    ) PS
,   (   SELECT  DECODE(LEVEL,1,'Y','N') AS is_gap
        FROM    DUAL
        CONNECT BY LEVEL <= 2
    ) G
WHERE   (   PS.prev_end > PS.prev_start
        OR  G.is_gap    = 'N' )
ORDER BY 3
/

给我这些结果

CUST_ID I START_DATE END_DATE   PROVIDER    PER_KWH PER_GAL
------- - ---------- ---------- ----------- ------- -------
      1 Y 01-JAN-00  31-DEC-08
      1 N 01-JAN-09  31-JAN-10  LocalNuke   1.8     0
      1 N 01-FEB-10  31-MAR-10  CoalGuys    1.6     0
      1 Y 01-APR-10  31-MAY-10
      1 N 01-JUN-10  30-SEP-10  LocalNuke   1.7     0
      1 Y 01-OCT-10  28-FEB-11
      1 N 01-MAR-11  31-DEC-99  GeoHeat     1.1     0

一些注意事项:

  • 我认为您的示例结果的第 5 行的结束日期不正确。应该是31-JUL-2010,因为那是activity 结束的时间吗?
  • 我将CoalGuys 开始日期更新为01-FEB-2010 以测试何时没有间隙
  • 如果没有任何活动可以运行到遥远的未来,那么将被搞砸,因为它不会产生尾随间隙,而只会产生前面的间隙。我猜总是可以UNION 一个
  • 最好不要将9999 用作一年,因为如果您尝试向其中添加任何内容,则会出现错误。最终没关系,但如果你追赶差距,那就太麻烦了。

现在距离完整的解决方案还有很长的路要走,一旦你把客户和水日期都扔进去,它就会变得更加混乱。但是您可能需要将上述内容作为内联视图包含在主查询中。然后你必须对 WATER 做同样的事情。然后,您必须将两者与日期范围检查结合起来,然后使用 LEAST 和 GREATEST 作为最终日期结果。

抱歉,在我花了 40 分钟左右的时间解决这个问题后,它从一个有趣的问题变成了工作的感觉,所以我的答案不完整。希望对您有所帮助。

【讨论】:

  • 是的,关于我预期输出中的第 5 行不正确,您是对的。我现在会修复它。我需要一段时间来消化和尝试一些东西......但我想感谢你提供笔记和你的思考过程。即使您的 cmets 没有成功,他们也会帮助我了解这种使用 Analytics 做事的新思维方式。
猜你喜欢
  • 2020-02-28
  • 2013-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多