使用 SQL 来标识具有开始日期和结束日期的时间段
Using SQL to identify periods of time with start and end dates
我正在为一个研究项目准备一些数据,但遇到了以下挑战。如果可能的话,我想用 SQL 或 PL SQL 来做所有事情(虽然我是一个真正的 PL 新手)。
假设我们有以下 table(请注意 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 代码。 此问题的挑战是识别所有 Start/End 对以创建 Period_ID 列。此挑战的一个重要细微差别:End_period 代码如果在 Start_period 代码的 28 天内,则为 INVALID。例如,第 2 行中的 End_period 代码无效,因为它是在 1 月 15 日,距 1 月 1 日仅 14 天。相反,有效的 End_period 代码位于第 5 行,因为它已超过 28 天。
- 日期: 交易日期
- Period_ID: 所需的行 -- 此信息当前不在 table.
只需计算每一行的开始周期数:
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;
这适用于您提供的数据。它没有正式纳入其他规则,例如结束期间之间的时间安排。
这是一个使用总是有趣的 Match_Recognize 的答案。请注意,您实际上不应命名列 'Code' 或 'Date',因为它们是保留关键字。
Match_Recognize 对多行进行操作,并尝试匹配给定的模式。在您的情况下,您正在尝试匹配开始代码的模式,后跟零个或多个无效结束 codes/other 代码,然后是有效结束代码。
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
我会使用递归 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
逻辑是使用引导周期变化的列chg
。当代码为 End period
并且日期大于之前记住的开始日期时,Chg
设置为 1。在下一步中,周期增加,chg
重置为零并设置新的开始日期。
我正在为一个研究项目准备一些数据,但遇到了以下挑战。如果可能的话,我想用 SQL 或 PL SQL 来做所有事情(虽然我是一个真正的 PL 新手)。
假设我们有以下 table(请注意 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 代码。 此问题的挑战是识别所有 Start/End 对以创建 Period_ID 列。此挑战的一个重要细微差别:End_period 代码如果在 Start_period 代码的 28 天内,则为 INVALID。例如,第 2 行中的 End_period 代码无效,因为它是在 1 月 15 日,距 1 月 1 日仅 14 天。相反,有效的 End_period 代码位于第 5 行,因为它已超过 28 天。
- 日期: 交易日期
- Period_ID: 所需的行 -- 此信息当前不在 table.
只需计算每一行的开始周期数:
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;
这适用于您提供的数据。它没有正式纳入其他规则,例如结束期间之间的时间安排。
这是一个使用总是有趣的 Match_Recognize 的答案。请注意,您实际上不应命名列 'Code' 或 'Date',因为它们是保留关键字。
Match_Recognize 对多行进行操作,并尝试匹配给定的模式。在您的情况下,您正在尝试匹配开始代码的模式,后跟零个或多个无效结束 codes/other 代码,然后是有效结束代码。
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
我会使用递归 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
逻辑是使用引导周期变化的列chg
。当代码为 End period
并且日期大于之前记住的开始日期时,Chg
设置为 1。在下一步中,周期增加,chg
重置为零并设置新的开始日期。