SQL 更新 WHERE IN 子查询已停止工作 [重复]

Posted

技术标签:

【中文标题】SQL 更新 WHERE IN 子查询已停止工作 [重复]【英文标题】:SQL Update WHERE IN subquery has stopped working [duplicate] 【发布时间】:2021-04-14 18:38:43 【问题描述】:

我有这张桌子。 (还有其他字段,但 Id 和 Lang 在这里很重要。)

CREATE TABLE Request 
(
    Id int IDENTITY(1,1) NOT NULL,
    Lang nvarchar(20) NULL
    CONSTRAINT PK_MyTable PRIMARY KEY CLUSTERED (Id ASC)
) ON [PRIMARY]

我有一个用户定义的表类型。

CREATE TYPE dbo.IntList AS TABLE 
(
    Item int NOT NULL
)

最后,我有一个存储过程,它为我的表类型接收一个变量,以更新所有指定 Id 的 Lang 字段。

CREATE PROCEDURE BulkUpdLang
    @ids as IntList READONLY,
    @lang as nvarchar(20)
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE Request
    SET Lang = @lang
    WHERE Id IN (SELECT Item FROM @ids);
END

这工作正常,但由于某种原因已停止。它现在因错误而爆炸:

子查询返回超过 1 个值。当子查询跟随 =、!=、、>= 或子查询用作表达式时,这是不允许的

假设 MyTable 有 10 个 ID 为 1-10 的项目,这是我尝试过的:

declare @ids IntList;
insert into @ids
    select 3 union all
    select 4 union all
    select 5;

-- This is how I test the sproc
-- EXEC BulkUpdLang @ids, 'en-us';

-- This is just a select and works just fine.
--Select * from Request
--    Where Id in (Select Item From @ids);

-- This WAS working, but now throws the subquery error.
Update Request
Set Lang = 'en-us'
Where Id in (Select Item From @ids);

-- This works as expected. Unfortunately, the Ids need to be a variable.
--Update Request
--Set Lang = 'en-us'
--Where Id in (3, 4, 5);

-- This also throws the subquery error.
--Merge Request as Req
--Using @ids as Ids -- "Using (Select Item From @ids) as Ids" changes nothing
--on Req.Id = Ids.Item
--when matched then
--update set
--    Req.Lang = 'en-us';

-- This also throws the subquery error.
--Update Request
--Set Lang = 'en-us'
--From (Select Item From @ids) Ids
--Where Id = Ids.Item;

让我抓狂的是,这确实有效。我倾向于相信某些 SQL 服务器设置已经改变了它处理子查询的方式。有没有人有任何想法?我快疯了!


这个表上还有一个触发器:

ALTER TRIGGER [dbo].[Request_Status_Audit]
    ON [dbo].[Request]
    AFTER INSERT, UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @oldStatus nvarchar(15) = (SELECT [Status] FROM Deleted);
    DECLARE @newStatus nvarchar(15) = (SELECT [Status] FROM Inserted);
    
    IF (@oldStatus <> @newStatus) BEGIN
        INSERT INTO dbo.Audit_RequestStatus (RequestId, StatusDate, StatusValue)
            SELECT Inserted.Id, GETDATE(), Inserted.[Status]
            FROM Inserted;
    END

    -- data retention
    DELETE FROM dbo.Audit_RequestStatus
    WHERE StatusDate < DATEADD(YEAR, -6, GETDATE());

END

【问题讨论】:

此代码不会产生该错误。我敢打赌有人在桌子上创建了一个错误的触发器。 不要禁用触发器,修复它。 虽然你可以在 proc 中禁用触发器,但不要去那里。我同意@Larnu,你应该修复它(或放弃它)。 您的触发器不是基于集合的。对于超过 1 行的任何更新,这些变量的值是多少? 这是一个不考虑多行指令的 SQL Server 触发器的经典错误。与其将 id 提取到单个变量中,不如使用基于集合的构造来处理所有内容。 【参考方案1】:

您的触发器不处理多行插入和更新(也不处理 0 行),正如 by Brent Ozar (The Silent Bug I Find in Most Triggers) 和其他人所记录的那样。

CREATE OR ALTER TRIGGER [dbo].[Request_Status_Audit]
    ON [dbo].[Request]
    AFTER INSERT, UPDATE
AS 

    SET NOCOUNT ON;

    IF NOT UPDATE([Status]) OR NOT EXISTS
        (
            SELECT Id, [Status]
            FROM inserted
            EXCEPT
            SELECT Id, [Status]
            FROM deleted
        )
        RETURN ;
    
    INSERT INTO dbo.Audit_RequestStatus (RequestId, StatusDate, StatusValue)
        SELECT i.Id, GETDATE(), i.[Status]
        FROM (
            SELECT Id, [Status]
            FROM inserted
            EXCEPT
            SELECT Id, [Status]
            FROM deleted
        ) i;

    -- data retention
    DELETE FROM dbo.Audit_RequestStatus
    WHERE StatusDate < DATEADD(YEAR, -6, GETDATE());

GO

注意事项:

比较inserteddeleted 中的必要列以查看是否有更改,还要检查UPDATE 函数(注意UPDATE 只告诉您该列是否存在于查询中,而不是数据是否实际存在已更改),如果可以,请尽早退出。 在您的 INSERT 中再次检查更改的列 我强烈建议将DELETE 转移到代理工作中。

【讨论】:

【参考方案2】:

我的问题是一个带有子查询的触发器,我错误地认为总会有一个结果。显然,我陷入了一个经典的陷阱。根据人们提供的 cmets 和链接,我将触发器更新为此,现在一切正常。

INSERT INTO dbo.Audit_RequestStatus (RequestId, StatusDate, StatusValue)
    SELECT Inserted.Id, GETDATE(), Inserted.[Status]
    FROM Inserted
    JOIN Deleted ON Inserted.Id = Deleted.Id
    WHERE Inserted.[Status] <> Deleted.[Status];

感谢 Dan Guzman 和 Alejandro 帮助我开发此修复程序。

【讨论】:

这不会对 INSERT 执行任何操作(因为 INSERTED 表中没有记录)。如果这是正确的,触发器应该只为 UPDATE 定义,但目前它也在 INSERT 上触发。此外,如果状态为 NULLable,则此代码无法处理(尽管原始代码也没有)。 像我一样尝试使用基于集合的运算符,例如EXCEPT,这样可以避免@MatBailie 提到的问题

以上是关于SQL 更新 WHERE IN 子查询已停止工作 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 WHERE IN 子查询优化 SQL 查询

SQL 效率:WHERE IN 子查询与 JOIN 然后 GROUP

WHERE col1,col2 IN (...) [使用复合主键的 SQL 子查询]

SQL 中 where 后面可不可以跟上子查询

在“where”子句中使用“in”运算符更新行

更新子查询(WHERE/FROM)