Использование SQL для определения периодов времени с датами начала и окончания

Я готовлю некоторые данные для исследовательского проекта, и у меня возникли проблемы со следующей задачей. Если возможно, я хотел бы сделать все с 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. Задача этой задачи состоит в том, чтобы идентифицировать все пары Start/End для создания столбца Period_ID. Один важный нюанс этой задачи: код End_period является INVALID, если он находится в пределах 28 дней кода Start_period. Например, код End_period в строке 2 недействителен, потому что он приходится на 15 января, всего через 14 дней после 1 января. Вместо этого действительный код End_period находится в строке 5, потому что это более чем через 28 дней.
  • Дата: дата транзакции.
  • Period_ID: требуемая строка — этой информации в данный момент нет в таблице.

person Andrew Bell    schedule 21.03.2019    source источник
comment
Кажется, это проблема пробелов и островов. На сайте уже есть несколько вопросов по таким темам для Oracle. Возможно, один из их ответов может помочь вам? Проверьте их   -  person APC    schedule 21.03.2019


Ответы (3)


Вот ответ с использованием всегда интересного 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
person Josh Eller    schedule 21.03.2019

Просто подсчитайте количество начальных периодов в каждой строке:

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;

Это работает для данных, которые вы представили. Формально он не включает другие правила, такие как время между окончанием периодов.

person Gordon Linoff    schedule 21.03.2019
comment
Почему вы написали Просто посчитайте, но используйте сумму? Вместо этого используйте функцию подсчета. - person akk0rd87; 21.03.2019
comment
@akk0rd87 . . . sum() ведет подсчет. На самом деле, count(*) на самом деле не нужно; это просто sum(1). Если бы Oracle поддерживал стандартный синтаксис, я бы использовал count(*) filter (where code = 'Start_period') . . . . - person Gordon Linoff; 21.03.2019
comment
Кто говорит о count(*) ? Я сказал о count(case... end) или count(decode(...)). - person akk0rd87; 21.03.2019

Я бы использовал рекурсивный 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

Логика заключается в использовании столбца chg, который управляет изменением периода. Chg устанавливается на 1, когда код End period и дата больше, чем ранее запомненная дата начала. На следующем шаге период увеличивается, chg сбрасывается до нуля и устанавливается новая начальная дата.

person Ponder Stibbons    schedule 21.03.2019