Как обрабатывать строки с изменениями статуса и без них в таблице истории

У меня есть таблица в SQL Server, которая отслеживает состояние различных идентификаторов с течением времени. Таблица включает в себя как исторические, так и текущие данные, и мне нужно написать запрос, который будет возвращать правильный статус и диапазоны дат для каждого идентификатора, независимо от того, изменился статус или нет.

Структура таблицы следующая:

CREATE TABLE #history_Card_status (
    HistoryData BIT,
    StatusUpdateDate DATETIME,
    CreateDate DATETIME,
    Id BIGINT,
    Status NVARCHAR(50)
);

INSERT INTO #history_Card_status (HistoryData, StatusUpdateDate, CreateDate, Id, Status)
VALUES
-- Current Data
(0, NULL, '2023-06-13 05:07:38.700', 222, 'Open'),
(0, NULL, '2021-07-16 00:44:46.740', 111, 'Closed'),
-- Historical Data
(1, '2024-08-20 21:10:57.093', '2021-07-16 00:44:46.740', 111, 'Closed'),
(1, '2024-07-05 13:22:04.220', '2021-07-16 00:44:46.740', 111, 'Closed'),
(1, '2024-07-05 13:13:02.133', '2021-07-16 00:44:46.740', 111, 'Inactive/Block'),
(1, '2024-07-02 03:01:12.467', '2021-07-16 00:44:46.740', 111, 'Inactive/Block'),
(1, '2024-06-24 02:12:00.773', '2021-07-16 00:44:46.740', 111, 'Inactive/Block'),
(1, '2024-06-24 02:12:00.687', '2021-07-16 00:44:46.740', 111, 'Inactive/Block'),
(1, '2024-06-24 02:00:46.040', '2021-07-16 00:44:46.740', 111, 'Open'),
(1, '2024-06-24 02:00:44.303', '2021-07-16 00:44:46.740', 111, 'Open'),
(1, '2024-04-14 11:57:21.133', '2021-07-16 00:44:46.740', 111, 'Open'),
(1, '2024-04-14 11:52:09.073', '2021-07-16 00:44:46.740', 111, 'Open'),
(1, '2024-07-01 04:28:08.213', '2023-06-13 05:07:38.700', 222, 'Open'),
(1, '2024-07-01 03:39:54.607', '2023-06-13 05:07:38.700', 222, 'Open'),
(1, '2024-04-24 11:18:59.380', '2023-06-13 05:07:38.700', 222, 'Open'),
(1, '2024-04-24 11:18:59.227', '2023-06-13 05:07:38.700', 222, 'Open');

Эта таблица содержит как исторические записи (отмеченные HistoryData = 1), так и текущие записи (отмеченные HistoryData = 0). Столбец StatusUpdateDate заполняется только для исторических записей, а столбец CreateDate присутствует во всех записях.

Проблема

Мне нужно создать запрос, который возвращает статус и соответствующие диапазоны дат для каждого идентификатора. Запрос должен обрабатывать как случаи изменения статуса, так и случаи, когда он не меняется. Ожидаемый результат должен выглядеть так:

StatusUpdateDate         StatusTillDate          Id      Status
-----------------------  ----------------------  ------  -----------
2024-07-05 13:13:02.133  2024-08-24 08:49:31.233  111     Closed
2024-06-24 02:00:46.040  2024-07-05 13:13:02.133  111     Inactive/Block
2021-07-16 00:44:46.740  2024-06-24 02:00:46.040  111     Open
2023-06-13 05:07:38.700  2024-08-24 08:49:31.233  222     Open

Моя попытка

Я начал с запроса, который работает в случаях изменения статуса:

WITH Step1 AS (
    SELECT 
        StatusUpdateDate = ISNULL(StatusUpdateDate, ISNULL(history.CreateDate, '1970-01-01 00:00:00.000')),
        Id,
        Status,
        lag_status = LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC), 
        ChangeFlag = CASE 
            WHEN Status <> LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC) 
            THEN 1 ELSE 0 END 
    FROM #history_Card_status AS history
)
SELECT 
    StatusUpdateDate,
    StatusTillDate = ISNULL(LAG(StatusUpdateDate) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC), DATEADD(DAY, 2, GETDATE())),
    Id,
    Status = ISNULL(LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC), lag_status)
FROM Step1
WHERE ChangeFlag = 1;

Этот запрос хорошо работает для идентификаторов, статус которых изменился, но я вижу только идентификаторы с изменениями, например:

StatusUpdateDate         StatusTillDate          Id      Status
-----------------------  ----------------------  ------  -----------
2024-07-05 13:13:02.133  2024-08-24 06:22:39.003  111     Closed
2024-06-24 02:00:46.040  2024-07-05 13:13:02.133  111     Inactive/Block
2021-07-16 00:44:46.740  2024-06-24 02:00:46.040  111     Open

Затем я попытался обработать строки, в которых изменений не произошло:

WITH Step1 AS (
    SELECT 
        StatusUpdateDate = ISNULL(StatusUpdateDate, ISNULL(history.CreateDate, '1970-01-01 00:00:00.000')),
        Id,
        Status,
        lag_status = LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC), 
        ChangeFlag = CASE 
            WHEN Status <> LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC) 
            THEN 1 ELSE 0 END,
        rownum = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC) 
    FROM #history_Card_status AS history
)
SELECT 
    StatusUpdateDate,
    StatusTillDate = ISNULL(LAG(StatusUpdateDate) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC), DATEADD(DAY, 2, GETDATE())),
    Id,
    ChangeFlag,
    Status = ISNULL(LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC), lag_status),
    rownum
FROM Step1
WHERE ChangeFlag = 1
OR (ChangeFlag = 0 AND rownum = 1); 

Эта попытка, однако, дает неутешительные результаты, такие как:

StatusUpdateDate         StatusTillDate          Id      ChangeFlag  Status
-----------------------  ----------------------  ------  ----------- -----------
2024-08-20 21:10:57.093  2024-08-24 16:44:22.340  111     0           NULL
2024-07-05 13:13:02.133  2024-08-20 21:10:57.093  111     1           Closed
2024-06-24 02:00:46.040  2024-07-05 13:13:02.133  111     1           Inactive/Block
2021-07-16 00:44:46.740  2024-06-24 02:00:46.040  111     1           Open
2024-07-01 04:28:08.213  2024-08-24 16:44:22.340  222     0           NULL

Что я ищу

Я ищу способ вернуть правильный статус и диапазоны дат для каждого идентификатора, в том числе без каких-либо изменений статуса, как показано в ожидаемых результатах.

Откуда берется дата 2024-08-24 для ID = 222?

The Impaler 22.08.2024 23:23

Выглядит как-то пусто и островно.

siggemannen 22.08.2024 23:45

Если вы ожидаете, что ChangeFlag будет 1 для исходных записей истории, то вы, вероятно, захотите обработать случай null с помощью чего-то вроде CASE WHEN Status <> ISNULL(LAG(Status) OVER (PARTITION BY Id ORDER BY StatusUpdateDate DESC) , N'') THEN 1 ELSE 0 END

AlwaysLearning 22.08.2024 23:47

@TheImpaler Посмотрите на DATEADD(DAY, 2, GETDATE())) во второй части функции isnull внутри StatusTillDate во внешнем запросе.

Shay Davidovitch 24.08.2024 21:51
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
2
4
76
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

  1. Сначала отмечаем строки («Пробелы и острова»), статус которых меняется (Flag New Group fngr=1).
    Порядок строк по StatusUpdateDate.
    Однако, поскольку для строк HistoryData StatusUpdateDate имеет значение null, и мы хотим переместить эти строки в конец порядка вывода, мы используем order by HistoryData desc, StatusUpdateDate. Вывод подзапроса показан ниже в тексте

  2. Для отфильтрованных строк (fgrn=1) мы берем StatusUpdateDate (или CreateDate для первой строки)
    и StatusTillDate в качестве ведущей даты в filtered строках (или DATEADD(DAY, 2, GETDATE()) для последней строки).

См. пример

select Id,Status
  ,case when prevStatus='New' then CreateDate
   else StatusUpdateDate
   end StatusUpdateDate  
  ,lead(StatusUpdateDate,1,DATEADD(DAY, 2, GETDATE()))
          over(partition by Id order by HistoryData desc,StatusUpdateDate) StatusTillDate 
from(
  select *
    ,lag(Status,1,'New')over(partition by Id order by HistoryData desc,StatusUpdateDate) prevStatus
    ,case when Status<>lag(Status,1,'New')over(partition by Id order by HistoryData desc,StatusUpdateDate) 
       then 1 
     else 0 
     end fngr
  from history_Card_status
)t
where fngr=1
Идентификатор Статус СтатусОбновлениеДата СтатусДоДата 111 Открыть 2021-07-16 00:44:46.740 2024-06-24 02:12:00.687 111 Неактивный/Блокировать 2024-06-24 02:12:00.687 2024-07-05 13:22:04.220 111 Закрыто 2024-07-05 13:22:04.220 2024-08-24 22:04:52.890 222 Открыть 2023-06-13 05:07:38.700 2024-08-24 22:04:52.890

рабочий пример

После фильтра с fngr=1 (флажок «Новая группа статуса») мы получаем

Данные истории СтатусОбновлениеДата СоздатьДата Идентификатор Статус предыдущий статус фнгр Истинный 2024-04-14 11:52:09.073 2021-07-16 00:44:46.740 111 Открыть Новый 1 Истинный 2024-06-24 02:12:00.687 2021-07-16 00:44:46.740 111 Неактивный/Блокировать Открыть 1 Истинный 2024-07-05 13:22:04.220 2021-07-16 00:44:46.740 111 Закрыто Неактивный/Блокировать 1 Истинный 2024-04-24 11:18:59.227 2023-06-13 05:07:38.700 222 Открыть Новый 1

Обновление 1. Интересный момент со StatusUpdateDate и StatusTillDate. Если сравните желаемый результат с ОП, приведите ответ @siggemannen и мой результат.

Вар Идентификатор Статус СтатусОбновлениеДата СтатусДоДата ОП 111 Открыть 2021-07-16 00:44:46.740 2024-06-24 02:00:46.040 ОП 111 Неактивный/Блокировать 2024-06-24 02:00:46.040 2024-07-05 13:13:02.133 Сиг 111 Открыть 2021-07-16 00:44:46.740 2024-06-24 02:00:46.040 Сиг 111 Неактивный/Блокировать 2024-06-24 02:12:00.687 2024-07-05 13:13:02.133 Вал 111 Открыть 2021-07-16 00:44:46.740 2024-06-24 02:12:00.687 Вал 111 Неактивный/Блокировать 2024-06-24 02:12:00.687 2024-07-05 13:22:04.220

Например
'2024-06-24 02:00:46.040' - это последняя точка, где статус установлен ='Открыто'
«2024-06-24 02:12:00.687» — это момент, когда статус изменился с «Открыто» на «Неактивно/Блокировано».

    Open       ?       Inactive/Block        ?            Close
^-----------^------^------------------^--------------^------------  
|           |      |                  |              |
Open        Open   Inactive/Block     Inactive/Block Close

У меня вопрос - где находится точка StatusTillDate?


Для справки. Вывод подзапроса

Данные истории СтатусОбновлениеДата СоздатьДата Идентификатор Статус предыдущий статус фнгр Истинный 2024-04-14 11:52:09.073 2021-07-16 00:44:46.740 111 Открыть Новый 1 Истинный 2024-04-14 11:57:21.133 2021-07-16 00:44:46.740 111 Открыть Открыть 0 Истинный 2024-06-24 02:00:44.303 2021-07-16 00:44:46.740 111 Открыть Открыть 0 Истинный 2024-06-24 02:00:46.040 2021-07-16 00:44:46.740 111 Открыть Открыть 0 Истинный 2024-06-24 02:12:00.687 2021-07-16 00:44:46.740 111 Неактивный/Блокировать Открыть 1 Истинный 2024-06-24 02:12:00.773 2021-07-16 00:44:46.740 111 Неактивный/Блокировать Неактивный/Блокировать 0 Истинный 2024-07-02 03:01:12.467 2021-07-16 00:44:46.740 111 Неактивный/Блокировать Неактивный/Блокировать 0 Истинный 2024-07-05 13:13:02.133 2021-07-16 00:44:46.740 111 Неактивный/Блокировать Неактивный/Блокировать 0 Истинный 2024-07-05 13:22:04.220 2021-07-16 00:44:46.740 111 Закрыто Неактивный/Блокировать 1 Истинный 2024-08-20 21:10:57.093 2021-07-16 00:44:46.740 111 Закрыто Закрыто 0 ЛОЖЬ нулевой 2021-07-16 00:44:46.740 111 Закрыто Закрыто 0 Истинный 2024-04-24 11:18:59.227 2023-06-13 05:07:38.700 222 Открыть Новый 1 Истинный 2024-04-24 11:18:59.380 2023-06-13 05:07:38.700 222 Открыть Открыть 0 Истинный 2024-07-01 03:39:54.607 2023-06-13 05:07:38.700 222 Открыть Открыть 0 Истинный 2024-07-01 04:28:08.213 2023-06-13 05:07:38.700 222 Открыть Открыть 0 ЛОЖЬ нулевой 2023-06-13 05:07:38.700 222 Открыть Открыть 0

Объяснение (например, см. другой ответ) значительно улучшит ваш ответ.

Dale K 23.08.2024 00:15

@ValNik Спасибо за ответ! У меня есть одна вещь, которая нуждается в улучшении: мне нужна максимальная дата для каждого статуса, чтобы соответствовать минимальной дате следующего статуса, например: Максимальное обновление статуса (Открыто) == Минимальное обновление статуса (Неактивно/Блокировать) 2024-06-24 02:00:46.040 == 2024-06-24 02:00:46.040 Максимальное обновление статуса (Неактивно/Блокировано) == Минимальное обновление статуса (Закрыто) 05.07.2024 13:13:02.133 == 05.07.2024 13:13:02.133 Не могли бы вы предложить решение этой проблемы?

Shay Davidovitch 25.08.2024 08:35
Ответ принят как подходящий

Это выглядит как стандартные пробелы и острова, с небольшими морщинками, вроде даты начала:

SELECT
    MIN(CASE WHEN groupflag = 0 THEN CreateDate ELSE StatusUpdateDate END) AS minDate
    , MAX(ISNULL(StatusUpdateDate, DATEADD(DAY, 2, GETDATE()))) AS maxDate
    , ID, status, groupFlag
FROM (
    SELECT *
        , SUM(flag) OVER(partition BY ID ORDER BY historydata DESC, statusupdatedate rows BETWEEN unbounded preceding AND CURRENT row) AS groupFlag
    FROM (
        SELECT
            CASE WHEN status <> lag(status) OVER(partition BY id ORDER BY historydata DESC, statusupdatedate) THEN 1 ELSE 0 END AS flag
            , *
        FROM #history_Card_status h
    ) x
) x
GROUP BY groupflag, id, status
ORDER BY id, groupflag DESC

Обычно я использую CASE WHEN something <> lag(something) then 1 else 0 end, который отвечает за первую строку.

Затем мы суммируем вышеуказанный флаг, чтобы создать группировку. Для повышения производительности я использую ROWS BETWEEN, в противном случае по умолчанию используется RANGE, что работает немного медленнее.

Наконец, я беру CreateDate для первой строки, чтобы создать дату привязки, и StatusUpdateDate для остальных.

Выход:

миндата МаксДата ИДЕНТИФИКАТОР статус группаФлаг 2024-07-05 13:22:04.220 2024-08-25 00:04:20.760 111 Закрыто 2 2024-06-24 02:12:00.687 2024-07-05 13:13:02.133 111 Неактивный/Блокировать 1 2021-07-16 00:44:46.740 2024-06-24 02:00:46.040 111 Открыть 0 2023-06-13 05:07:38.700 2024-08-25 00:04:20.760 222 Открыть 0

Спасибо за ваш ответ! У меня есть одна вещь, которая нуждается в улучшении: мне нужна максимальная дата для каждого статуса, чтобы соответствовать минимальной дате следующего статуса, например: Максимальное обновление статуса (Открыто) == Минимальное обновление статуса (Неактивно/Блокировать) 2024-06-24 02:00:46.040 == 2024-06-24 02:00:46.040 Максимальное обновление статуса (Неактивно/Блокировано) == Минимальное обновление статуса (Закрыто) 05.07.2024 13:13:02.133 == 05.07.2024 13:13:02.133 Не могли бы вы предложить решение этой проблемы?

Shay Davidovitch 25.08.2024 08:23

Вы можете использовать функцию Lead, чтобы получить минимальную дату следующей группы. Или я что-то упускаю?

siggemannen 25.08.2024 11:55

Спасибо за предложение! Не могли бы вы показать мне, как использовать функцию LEAD для выравнивания дат? Я не уверен, где это вписывается в ваш запрос.

Shay Davidovitch 25.08.2024 13:36

Я внес небольшую корректировку в ваш запрос: ^ Показывает, что никаких изменений не было сделано... SELECT MIN(CASE WHEN groupFlag = 0 THEN CreateDate ELSE StatusUpdateDatemin END) AS minDate, ...^ FROM ( SELECT *, LAG(StatusUpdateDate) OVER (PARTITION BY id ORDER BY HistoryData DESC, StatusUpdateDate) AS StatusUpdateDatemin, ...^ Идея состоит в том, что когда группа завершена, MIN во внешнем запросе выглядит на одну строку выше, поэтому для минимального значения группы 1 проверяется max группы 0. Пожалуйста, дайте мне знать, правильный ли этот подход, или если у вас есть какие-либо предложения!

Shay Davidovitch 25.08.2024 15:39

Да, это тоже должно сработать. Я подумывал сделать другой уровень подзапроса и взять там LEAD следующей даты, но твой тоже звучит хорошо

siggemannen 25.08.2024 21:22

Ваш ответ очень помог! Большое Вам спасибо!

Shay Davidovitch 27.08.2024 19:37

Другие вопросы по теме