【问题标题】:Using SQL to identify periods of time with start and end dates使用 SQL 识别具有开始日期和结束日期的时间段
【发布时间】:2019-03-21 13:40:40
【问题描述】:

我正在为一个研究项目准备一些数据,但在应对以下挑战时遇到了麻烦。如果可能的话,我想用 SQL 或 PL SQL 来做所有事情(尽管我是一个真正的 PL 新手)。

假设我们有下表(注意 Period_ID 是我要创建的所需行):

+-------+-----------+--------------+--------------+-----------+
| Row # | Person_ID |     Code     |     Date     | Period_ID |
+-------+-----------+--------------+--------------+-----------+
|     1 |         1 | Start_period | Jan 1st      |         1 |
|     2 |         1 | End_period   | Jan 15th     |         1 |
|     3 |         1 | Random_code1 | Feb 15th     |         1 |
|     4 |         1 | Random_code2 | Feb 28th     |         1 |
|     5 |         1 | End_period   | March 31st   |         1 |
|     6 |         1 | Start_period | May 31st     |         2 |
|     7 |         1 | End_period   | June 11th    |         2 |
|     8 |         1 | End_period   | October 28th |         2 |
+-------+-----------+--------------+--------------+-----------+

专栏和挑战:

  • Person_ID:以上数据均针对一个人(该数据属于交易级别)。
  • 代码:此代码可以是 Start_period、End_period 或任何随机代码。每个 Start_period 代码都应该有一个对应的 End_period 代码。 此问题的挑战是识别所有开始/结束对以创建 Period_ID 列。 此挑战的一个重要细微差别:如果 End_period 代码在其中,则它是 INVALID Start_period 代码的 28 天。例如,第 2 行中的 End_period 代码无效,因为它是在 1 月 15 日,距离 1 月 1 日仅 14 天。相反,有效的 End_period 代码位于第 5 行,因为它晚了 28 天以上。
  • 日期:交易日期
  • Period_ID:所需的行 -- 该信息当前不在表格中。

【问题讨论】:

  • 这似乎是一个空白和孤岛问题。该站点已经针对 Oracle 提出了一些关于此类主题的问题。也许他们的答案之一可以帮助你? Check them out

标签: sql oracle gaps-and-islands


【解决方案1】:

这是一个使用总是很有趣的 Match_Recognize 的答案。请注意,您实际上不应将列命名为“代码”或“日期”,因为它们是保留关键字。

Match_Recognize 对多行进行操作,并尝试匹配给定的模式。在您的情况下,您尝试匹配起始码的模式,后跟零个或多个无效的结束码/其他代码,然后是有效的结束码。

WITH test_vals AS (
    SELECT 1 as person_ID,'Start_period' as my_code,to_date('Jan 1','mon dd') as my_date FROM DUAL
    UNION ALL SELECT 1,'End_period',to_date('Jan 15','mon dd') FROM DUAL
    UNION ALL SELECT 1,'Random_code1',to_date('Feb 15','mon dd') FROM DUAL
    UNION ALL SELECT 1,'Random_code2',to_date('Feb 28','mon dd') FROM DUAL
    UNION ALL SELECT 1,'End_period',to_date('March 31','mon dd') FROM DUAL
    UNION ALL SELECT 1,'Start_period',to_date('May 31','mon dd') FROM DUAL
    UNION ALL SELECT 1,'End_period',to_date('June 11','mon dd') FROM DUAL
    UNION ALL SELECT 1,'End_period',to_date('October 28','mon dd') FROM DUAL
)

SELECT m.person_id,
       m.my_code,
       m.my_date,
       m.period_id
FROM test_vals t
match_recognize(
    PARTITION BY person_id
    ORDER BY my_date
    MEASURES
        match_number() AS period_id /* Return the match number as the period ID */
    ALL ROWS PER match
    pattern (
        start_code /* Match a single start code */
        (invalid_end_code | other_code)* /* Match zero or more invalid end codes or other codes */
        valid_end_code /* Match a single end code */
    )
    define
        start_code AS my_code = 'Start_period', /* Start codes are always valid */
        valid_end_code AS my_code = 'End_period' AND (my_date - FIRST(my_date)) > 28, /* End codes are only valid if they come more than 28 days after the start of the pattern match */
        invalid_end_code AS my_code = 'End_period' AND (my_date - FIRST(my_date)) <= 28,
        other_code AS my_code NOT IN ('Start_period', 'End_period')
) m

【讨论】:

    【解决方案2】:

    只需计算每一行的起始周期数:

    select t.*,
           sum(case when code = 'Start_period' then 1 else 0 end) over (partition by person_id order by date) as period_id
    from t;
    

    这适用于您提供的数据。它没有正式纳入其他规则,例如结束周期之间的时间。

    【讨论】:

    • 为什么你写“Just count”却用“sum”?请改用计数函数。
    • @akk0rd87 。 . . sum() 正在计数。实际上,count(*) 并不是必需的。它只是sum(1)。如果 Oracle 支持标准语法,那么我会使用 count(*) filter (where code = 'Start_period') . . .
    • 谁在谈论“count(*)”?我说的是“count(case ... end)”或“count(decode(...))”。
    【解决方案3】:

    我会使用递归 cte,就像这里:

    with c(row_, code, date_, st_date, period, chg) as (
        select row_, code, date_, date_, 1, 0 from t where row_ = 1
        union all
        select t.row_, t.code, t.date_, 
               case when chg = 1 then t.date_ else st_date end, 
               case when chg = 1 then period + 1 else period end, 
               case when t.code = 'End_period' and t.date_ - c.st_date > 28 then 1 else 0 end
          from t join c on t.row_ = c.row_ + 1
        )
    select row_, code, date_, period from c
    

    dbfiddle demo

    逻辑是使用引导周期变化的列chg。当代码为 End period 且日期大于先前记住的开始日期时,Chg 设置为 1。在下一步增加期间,chg 重置为零并设置新的开始日期。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-05
      • 2021-09-06
      • 1970-01-01
      • 2022-10-13
      • 2022-12-01
      相关资源
      最近更新 更多