如何在不使用循环的情况下更新 SQL Server 中两组不同条件之间的行

Posted

技术标签:

【中文标题】如何在不使用循环的情况下更新 SQL Server 中两组不同条件之间的行【英文标题】:How to update rows between two different sets of criteria in SQL Server without using a loop 【发布时间】:2019-11-09 15:47:20 【问题描述】:

问题: (SQL Server 2014)。换句话说,对于结果集中的每一行,如何更新第一次出现(使用一个标准)和第二次出现(使用不同的标准)之间的每一行。我认为部分问题是尝试对查询中的每一行运行 TOP N 查询。

具体: 在下面的示例起始表中,如何更新最后两列日期:

    如果空类别行前面有一个“S”类别,则更新空类别行和最后一个连续的“M”类别行之间的行。类别可以包含“S”、“M”或 null 的任意顺序。

    设置 StartDate = IDEndDate+空行之前“S”行的 1 天。

    将最后一行的 EndDate = IDEndDate 设置为“M”类别。

这是SQLFiddle。

注意事项:我过去曾使用循环 (fetch..) 完成此操作,但我尝试通过一些查询来完成此操作,而不是类似于:

第 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

【问题讨论】:

【参考方案1】:

这是对这个 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 的测试here

【讨论】:

感谢您的尝试。你的输出很接近。第一组的 StartDate 提前一天。 我在起始表和所需结束表中添加了三行以显示可变性。正如您提到的,您的代码将无法处理某些条件。 @user610064 新版本使用了稍微不同的方法。并适用于当前示例。不是很完美,因为我可以想象一些边缘情况仍然会搞砸。但比以前的解决方案更接近。 令人印象深刻。我添加了另外两个 ID 以显示可变性。您的代码获得了正确的前两个 ID (11, 22),但不是后两个 ID (33, 44)。谢谢! 感谢您的帮助。我发布了我找到的答案。【参考方案2】:

我回答了我自己的问题。我有两个主要错误:

1) 前 N 个查询需要交叉应用(或外部应用)才能正常工作。 使用交叉应用,将为来自内部查询的每一行运行前 N 个查询。 使用内连接(或左连接),所有行都将首先从内查询返回,前 N 个查询只运行一次。

2) 过滤“[column] 'M'” 搞砸了我,因为它没有排除 NULL。我不得不改用“[column] = 'S' or [column] is null”

在rextester中找到的最终SQL

下面的工作代码:

;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

【讨论】:

以上是关于如何在不使用循环的情况下更新 SQL Server 中两组不同条件之间的行的主要内容,如果未能解决你的问题,请参考以下文章

如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?

如何在不使用数据透视和反透视的情况下在 SQL Server 中水平显示数据?

如何在不使用pivot和unpivot的情况下在SQL Server中水平显示数据?

如何在不使用 SQL Server/MS Access 的所有选定列的情况下进行分组和求和?

如何在不使用 Docker 或 Windows Server 2016 上的 Confluent 平台的情况下在 Kafka 中设置 Debezium SQL Server 连接器?

SQL Server,如何在不丢失数据的情况下创建表后设置自动增量?