Как обновить строки между двумя различными наборами критериев в SQL Server без использования цикла - PullRequest
0 голосов
/ 09 ноября 2019

Проблема : Как обновить строки между двумя различными наборами критериев в SQL Server без использования цикла (SQL Server 2014). Другими словами, для каждой строки в наборе результатов, как обновить каждую строку между первым вхождением (с одним критерием) и вторым вхождением (с разными критериями). Я думаю, что часть проблемы заключается в попытке выполнить запрос TOP N для каждой строки в запросе.

Конкретно : в приведенной ниже примере стартовой таблицы, как я могу обновить последние 2 столбцадат, где:

  1. Обновление строк между пустыми строками категории и последней последовательной строкой категории "M", если нулевой строке категории предшествует категория "S". Категория может содержать любой порядок «S», «M» или ноль.

  2. Установить StartDate = IDEndDate + 1 день строки «S», предшествующей пустой строке.

  3. Установить EndDate = IDEndDate последней строки с категорией «M».

Вот SQLFiddle .

Примечания : я делал это в прошлом с помощью цикла (выборка ..), но я пытаюсь сделать это с помощью нескольких запросов, например:

шаг 1: Получить работу: выбрать все допустимые пустые строки (начало диапазона)

шаг 2: для каждой вышеупомянутой строки выберите соответствующую последнюю строку "M" (конец диапазона), а затем выполнить запрос для обновления StartDate, EndDates в каждом диапазоне.

Starting Table:
ID  IDStartDate IDEndDate   Category
------------------------------------
11  2017-01-01  2017-01-31  S
11  2017-02-02  2017-02-03  null
11  2017-02-03  2017-03-31  M
11  2017-04-01  2017-04-30  M
22  2017-05-01  2017-06-15  S
22  2017-06-16  2017-06-20  null
22  2017-06-21  2017-06-25  M
22  2017-06-26  2017-06-27  null
22  2017-06-28  2017-06-29  S
22  2017-06-30  2017-07-05  M
33  2017-06-30  2017-07-14  M
33  2017-07-15  2017-07-20  S
33  2017-07-21  2017-07-25  null
44  2018-06-30  2018-07-14  S
44  2018-07-15  2018-07-20  M
44  2018-07-21  2018-07-25  null


Desired Ending Table:
ID  IDStartDate IDEndDate  Category StartDate   EndDate 
----------------------------------------------------------
11  2017-01-01  2017-01-31 S        
11  2017-02-02  2017-02-03 null     2017-02-01  2017-04-30  
11  2017-02-03  2017-03-31 M        2017-02-01  2017-04-30
11  2017-04-01  2017-04-30 M        2017-02-01  2017-04-30
22  2017-05-01  2017-06-15 S        
22  2017-06-16  2017-06-20 null     2017-06-16  2017-06-25  
22  2017-06-21  2017-06-25 M        2017-06-16  2017-06-25
22  2017-06-26  2017-06-27 null
22  2017-06-28  2017-06-29 S
22  2017-06-30  2017-07-05 M
33  2017-06-30  2017-07-14 M
33  2017-07-15  2017-07-20 S
33  2017-07-21  2017-07-25 null
44  2018-06-30  2018-07-14 S
44  2018-07-15  2018-07-20 M
44  2018-07-21  2018-07-25 null

Ниже приведен SQL-код для создания таблицы и просмотра результатов запроса, которые я начал. Я пробовал cte, поперечное применение, внешнее применение, внутренние соединения ... без удачи. Большое спасибо!

CREATE TABLE test (
    ID INT,
    IDStartDate date,
    IDEndDate date,
    Category VARCHAR (2),
    StartDate date,
    EndDate date
);
INSERT INTO test (ID, IDStartDate, IDEndDate, Category)
VALUES 
 (11, '2017-01-01', '2017-01-31', 'S')
,(11, '2017-02-02', '2017-02-03', null) 
,(11, '2017-02-03', '2017-03-31', 'M') 
,(11, '2017-04-01', '2017-04-30', 'M') 
,(22, '2017-05-01', '2017-06-15', 'S')
,(22, '2017-06-16', '2017-06-20', null)
,(22, '2017-06-21', '2017-06-25', 'M')
,(22, '2017-06-26', '2017-06-27', null)
,(22, '2017-06-28', '2017-06-29', 'S')
,(22, '2017-06-30', '2017-07-05', 'M')
,(33, '2017-06-30', '2017-07-14', 'M')
,(33, '2017-07-15', '2017-07-20', 'S')
,(33, '2017-07-21', '2017-07-25', null)
,(44, '2018-06-30', '2018-07-14', 'S')
,(44, '2018-07-15', '2018-07-20', 'M')
,(44, '2018-07-21', '2018-07-25', null);


--**************************
--results: shows first rows of each range
--**************************
;with cte as
(
select *
,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS RowNum
,LAG(IDEndDate) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastIDEndDate
,LAG(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastCategory
,LEAD(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS nextCategory
from test
)
select *  --select first row of each range to update
from cte
where Category is null and lastCategory = 'S' and nextCategory = 'M'


--*******************************
--6 of 8 "new" values are correct (missing NewEndDate for first range)
--*******************************
;with cte as
(
SELECT *
,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS RowNum
,LAG(IDEndDate) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastIDEndDate
,LAG(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastCategory
,LEAD(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS nextCategory
FROM test
), cte2 as
(
select *        --find the first/start row of each range
,LAG(RowNum) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastRowNum
,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', DateAdd(day, 1, lastIDEndDate), null) as NewStartDate
,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', RowNum, null) as NewStartRowNum
from cte
)
select t1.*, t3.*
from cte2 t1
outer apply
(       
  select top 1   --find the last/ending row of each range
   t2.lastIDEndDate as NewEndDate  
  ,t2.lastRowNum as NewEndRowNum
  from cte2 t2
  where t1.ID = t2.ID
  and t1.NewStartRowNum < t2.RowNum
  and t2.nextCategory <> 'M'  
  order by t2.ID, t2.RowNum
) t3
order by t1.ID, t1.RowNum

Ответы [ 2 ]

0 голосов
/ 14 ноября 2019

Я ответил на свой вопрос. У меня было две основные ошибки:

1) Для правильной работы запроса Top N требуется перекрестное применение (или внешнее применение). При перекрестном применении запрос Top N будет выполняться для каждой строки внутреннего запроса. Используя внутреннее соединение (или левое соединение), все строки будут возвращаться первыми из внутреннего запроса, а запрос Top N выполняется только один раз.

2) Фильтрация по "[column] <> 'M'" испорченаменя, поскольку это не исключало NULL. Мне пришлось использовать вместо этого «[column] = 'S' или [column] имеет значение null»

Окончательный SQL найден в rextester

Рабочий код ниже:

;with cte as
(
SELECT *
  ,ROW_NUMBER() OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS RowNum
  ,LAG(IDEndDate) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastIDEndDate
  ,LAG(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS lastCategory
  ,LEAD(Category) OVER(PARTITION BY ID ORDER BY ID, IDStartDate, IDEndDate) AS nextCategory
FROM test
), cte2 as
(
select t1.ID, t1.IDStartDate, t1.IDEndDate   --find the first/start row of the range
  ,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', DateAdd(day, 1, lastIDEndDate), null) as NewStartDate
  ,IIF(Category is null and lastCategory = 'S' and nextCategory = 'M', RowNum, null) as NewStartRowNum
  ,t3.*
from cte t1
   cross apply
   (       
   select top 1   --find the last/ending row of the range
     t2.IDEndDate as NewEndDate  
    ,t2.RowNum as NewEndRowNum
   from cte t2
   where t1.ID = t2.ID
   and t1.RowNum < t2.RowNum
   and (t2.nextCategory ='S' or t2.nextCategory is null)
   order by t1.ID, t1.RowNum
) t3
where Category is null and lastCategory = 'S' and nextCategory = 'M'
)
update t4
set StartDate = NewStartDate
   ,EndDate = NewEndDate
from cte t4
inner join cte2 t5
on t4.ID = t5.ID
and t4.RowNum Between NewStartRowNum and NewEndRowNum

select * from test
0 голосов
/ 09 ноября 2019

Вот попытка этой загадки SQL.

По сути, он обновляется из CTE.

Сначала рассчитывается накопленная сумма. Чтобы создать какой-то рейтинг.

Тогда только для рангов 2 и 3 будут рассчитываться даты.

;WITH CTE AS
(
    SELECT ID, IDStartDate, IDEndDate, Category, StartDate, EndDate,
    DATEADD(day,1, FIRST_VALUE(IDEndDate) OVER (PARTITION BY ID ORDER BY IDStartDate)) AS NewStartDate,
      FIRST_VALUE(IDEndDate) OVER (PARTITION BY ID ORDER BY IDStartDate DESC) AS NewEndDate
    FROM
    (
        SELECT ID, IDStartDate, IDEndDate, Category, StartDate, EndDate,
        SUM(CASE WHEN Category = 'S' THEN 2 WHEN Category IS NULL THEN 1 END) OVER (PARTITION BY ID ORDER BY IDStartDate) AS cSum
        FROM test t
    ) q
    WHERE cSum IN (2, 3)
)
UPDATE CTE
SET
    StartDate = NewStartDate, 
    EndDate = NewEndDate
WHERE (Category IS NULL OR Category = 'M');

Тест на rextester здесь

...