Подсчитайте последовательные значения один раз в TSQL

#sql #sql-server #tsql

Вопрос:

У меня есть таблица, в которой у меня есть данные о курьерской доставке. У меня доставка запланирована на каждый день. Я хочу подсчитать количество неудачных поставок. Если доставка не удалась в последующие дни, то она должна быть засчитана как 1. Например, доставка запланирована на каждый день апреля 2021 года. Доставка не состоялась в день 15 апреля. После этого доставка снова не удалась с 18 по 20 апреля. Хотя количество дней, в течение которых доставка не удалась, составляет 4 дня, но я хочу посчитать количество сбоев как 2, потому что непрерывные дни сбоев будут засчитываться как 1.

 DeliveryId     DeliveryDate     Status
1              2021-04-14       Success
2              2021-04-15       Failure
3              2021-04-16       Success
4              2021-04-17       Success
5              2021-04-18       Failure
6              2021-04-19       Failure
7              2021-04-20       Failure
8              2021-04-21       Success
 

Мне нужна новая колонка, в которой будет показан идентификатор доставки первого сбоя, как показано ниже.

 DeliveryId     DeliveryDate     Status     FailedDeliveryId
1              2021-04-14       Success     
2              2021-04-15       Failure     2
3              2021-04-16       Success     
4              2021-04-17       Success     
5              2021-04-18       Failure     5
6              2021-04-19       Failure     5
7              2021-04-20       Failure     5
8              2021-04-21       Success     
 

Я попробовал несколько вариантов, но не смог достичь вышеуказанного результата. Я использовал функцию ЗАДЕРЖКИ в своем запросе, чтобы найти предыдущий статус доставки. Но проблема в том, что если доставка не выполняется более 3 или 4 дней, мне придется использовать функцию ЗАДЕРЖКИ на 3 или 4 дня назад и проверить статус. Я хочу сделать это динамическим запросом. Ниже приведено то, что я использовал

 SELECT *, 
       CASE WHEN Status='Failure' AND Prev_Status='Success' THEN DeliveryId 
            WHEN Status='Failure' AND Prev_Status='Failure' THEN Prev_DeliveryId 
            END AS FailureInstance 
FROM (
         SELECT *, 
                LAG(Status,1) OVER(ORDER BY DeliveryDate ASC) Prev_Status,
                LAG(DeliveryId,1) OVER(ORDER BY DeliveryDate ASC) Prev_DeliveryId
         FROM   table1 
     ) A
 

Комментарии:

1. Не могли бы вы, пожалуйста, показать свою попытку ?

2. Я уже упоминал о том, что я пытался сделать до сих пор. Пожалуйста, ваше руководство необходимо

3.Попробуй DENSE_RANK docs.microsoft.com/en-us/sql/t-sql/functions/…

Ответ №1:

 declare @t table(DeliveryId int, DeliveryDate date, Status varchar(10));

insert into @t(DeliveryId, DeliveryDate, Status)
values
(10, '2021-04-14', 'Success'),
(20, '2021-04-15', 'Failure'),
(30, '2021-04-16', 'Success'),
(40, '2021-04-17', 'Success'),
(70, '2021-04-18', 'Failure'),
(60, '2021-04-19', 'Failure'),
(50, '2021-04-20', 'Failure'),
(80, '2021-04-21', 'Success');


select *, 
case when Status='Failure' then min(grpDeliveryId) over(partition by grp) end as FailedDeliveryId,
case when Status='Failure' then datediff(minute, min(DeliveryDate) over(partition by grp), min(grpSuccessDate) over(partition by grp) ) end as MinutesDiffFailSuccess
from
(
select *, 
sum(addorcountme) over(order by DeliveryDate) as grp
from
(
select *, 
case when Status='Failure' and lag(Status) over(order by DeliveryDate)='Failure' then null else 1 end as addorcountme,
case when Status='Failure' and lag(Status) over(order by DeliveryDate)='Failure' then null else DeliveryId end as grpDeliveryId,
case when Status='Failure' and lead(Status) over(order by DeliveryDate)='Success' then lead(DeliveryDate) over(order by DeliveryDate) end as grpSuccessDate
from @t
) as t
) as g;



/*
select *, case when Status='Failure' then min(DeliveryId) over(partition by grp) end as FailedDeliveryId
from
(
select *, 
sum(sumorcountme) over(order by DeliveryDate) as grp
from
(
select *, case when Status='failure' and lag(Status) over(order by DeliveryDate)='Failure' then null else 1 end as sumorcountme
from @t
) as t
) as g; */
 

Комментарии:

1. Мне кажется, что это работает так, как и ожидалось.

2. Этот работает так, как и ожидалось. Мне также нужно рассчитать время между Датой Первого сбоя и последующей Датой Первого успеха. Это сообщит мне общее время, в течение которого доставка не была выполнена. Можете ли вы изменить свой запрос, чтобы рассчитать и это?

3. …@marcin2x4 .. это не будет работать правильно, если идентификаторы доставки не сопоставлены с датами… попробуйте переключить ids..eg назначьте идентификатор 70 на 18 апреля и идентификатор 50 на 20 апреля..

Ответ №2:

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

 select t.*,
       (case when status = 'Failure'
             then min(case when status = 'Failure' then DeliveryId end) over (partition by grp)
        end) as first_failureId
from (select t.*,
             sum(case when status = 'Success' then 1 else 0 end) over (order by DeliveryDate) as grp
      from t
     ) t;
 

Вот скрипка db<>.

На самом деле, немного более простая версия назначает группы в обратном порядке, поэтому первая запись является неудачной, а не успешной, поэтому case в этом min() нет необходимости:

 select t.*,
       (case when status = 'Failure'
             then min(DeliveryId) over (partition by grp)
        end) as first_failureId
from (select t.*,
             sum(case when status = 'Success' then 1 else 0 end) over (order by DeliveryDate desc) as grp
      from t
     ) t
order by DeliveryId;
 

Хммм…. другой подход используется lag() для обнаружения изменений состояния. Затем используйте накопительный максимум только для изменений:

 select t.*,
       (case when status = 'Failure'
             then max(case when prev_status is null or prev_status <> status then DeliveryId end) over (order by DeliveryDate)
        end) as first_failureId
from (select t.*,
             lag(status) over (order by DeliveryDate) as prev_status
      from t
     ) t
order by DeliveryId;
 

Ответ №3:

Это вопрос «пробелов и островов». Моя любимая статья-это https://blog.jooq.org/2016/04/25/10-sql-tricks-that-you-didnt-think-were-possible/, совет №4.

Ключом к разрешению островов является использование ключа(т. е. даты) — row_number, и это группирует острова с одинаковым номером. Результат аналогичен вашему, но он будет работать с любым количеством сбоев без использования lag . Вы хотите использовать только ошибки здесь:

 select *,DeliveryDate-row_number() over (order by DeliveryId asc) as grp
from table1
where Status='Failure'
 

Теперь, когда у вас это есть, вы можете просто добавить dense_rank() поверх (порядок по grp), чтобы получить количество неудач, возможно, также объединить успехи:

 ;with cte as
(
    select *,DeliveryDate-row_number() over (order by DeliveryId asc) as grp
    from table1
    where Status='Failure'
)
    select 
        cte.*,dense_rank() over (order by grp) as FailureNum
    from cte
union all
    select *,null as grp,null as FailureNum
    from table1
    where Status='Success'
 

Ответ №4:

Вот моя незавершенная работа. Нужно найти обходной путь для повторного запуска подсчета, как только Status изменения вернутся к Success

Поиграйте с ним, используя https://dbfiddle.uk/

 CREATE TABLE test
(
    DeliveryId int NOT NULL,
    DeliveryDate date NOT NULL,
    Status varchar(max) NOT NULL)

INSERT INTO test (DeliveryId,DeliveryDate,Status) 
values 
    (1, '2021-04-14', 'Success'),
    (2, '2021-04-15', 'Failure'),
    (3, '2021-04-16', 'Success'),
    (4, '2021-04-17', 'Success'),
    (5, '2021-04-18', 'Failure'),
    (6, '2021-04-19', 'Failure'),
    (7, '2021-04-20', 'Failure'),
    (8, '2021-04-21', 'Success')

with cte as
(
  SELECT DeliveryId,
         DeliveryDate,
         Status,
         1 as FailedDeliveryId
  FROM test
  WHERE DeliveryId = 1
    UNION all
  SELECT t.DeliveryId,
         t.DeliveryDate,
         t.Status,
         cte.FailedDeliveryId   CASE WHEN cte.Status <> t.Status THEN 1 ELSE 0 END
  FROM test as t INNER JOIN cte ON t.DeliveryId - 1 = cte.DeliveryId
)


SELECT 
       DeliveryId,
       DeliveryDate,
       Status,
       FailedDeliveryId
FROM cte       
ORDER BY DeliveryId