【问题标题】:Sql Server Trigger Fires Square Number of TimesSql Server 触发器触发 Square 次数
【发布时间】:2021-10-20 12:15:44
【问题描述】:

我正在尝试向记录更改的某些行的值的表添加触发器。一切都按预期工作,除了触发器执行行数的平方。例如,如果我要在批量查询中更改表中 5 个项目的名称,触发器将在 TriggerTable 中记录 25 个条目。每个项目都会记录更改的总项目数,因此第一个名称更改将被记录 5 次,因为总共有 5 次名称更改。在下面粘贴了触发器的一部分。

我尝试添加IF TRIGGER_NESTLEVEL() <= 1 没有成功。除了这部分之外,其余功能都很好用。任何建议表示赞赏!

CREATE OR ALTER TRIGGER [dbo].[Trigger]
ON [dbo].[Table]
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
    IF UPDATE(Name)
        IF EXISTS(SELECT Name FROM inserted) AND EXISTS(SELECT Name FROM deleted)
            INSERT INTO TriggerTable (Id, ColumnName, OldValue, NewValue)
                SELECT i.Id, 'Name', d.Name, i.Name
                FROM deleted d, inserted i
                WHERE d.Name <> i.Name OR (d.Name IS NULL AND i.Name IS NOT NULL) OR (d.Name IS NOT NULL AND i.Name IS NULL)
        ELSE IF EXISTS(SELECT Name FROM inserted)
            INSERT INTO  TriggerTable (Id, ColumnName, NewValue)
                SELECT i.Id, 'Name', i.Name
                FROM inserted i
        ELSE IF EXISTS(SELECT Name FROM deleted)
            INSERT INTO  TriggerTable (Id, ColumnName, OldValue)
                SELECT d.Id, 'Name', d.Name
                FROM deleted d

【问题讨论】:

  • "除了触发器执行行数的平方倍" 它不会“执行”那么多次,它会触发一次;每个 DML 语句一个触发器只执行一次,并不多。至于问题是因为你的 1980 年代的 CROSS JOIN 在 inserted 和 deleted 上。我建议使用 1992 INNER JOIN。
  • IF UPDATE 不会像您认为的那样做。许多人误解了它实现的逻辑——一个可以理解但常见的错误。如果您的逻辑取决于对列值的实际更改,则您必须将旧值与新值进行比较以了解这一事实。对于插入语句,所有内容都会隐式更新。为什么您的触发器被定义为删除没有意义,但这是一个不同的问题。
  • 考虑到逻辑上的差异,最好有单独的INSERT 和UPDATE 触发器
  • @SMor 你说的是真的,但是我认为这里的使用是合适的——他们使用它来跳过任何不涉及该列的更新,并且至少对于更新案例他们尝试 只记录值发生变化的行(逻辑不太正确)。我不想考虑的是,是否有计划为表中的每一列创建这样的代码块。
  • @TCR 您是否考虑过更改跟踪、更改数据捕获、临时表或任何内置解决方案来阻止您完成所有这些繁重(且容易出错)的工作?

标签: sql sql-server database triggers


【解决方案1】:

@Larnu 是正确的,您的连接语法不是最佳的,但它向您隐藏了潜在的问题(问题不在于触发器执行了多次)。

您当前的逻辑与名称不同的任何行匹配。如果您更新两行,除非所有四个名称前后都相同,否则它们都会匹配。您需要匹配名称不同的任何行,但仅限于相同的Id。作为使用#temp 表和每个可能场景的示例(名称更改,名称不变,名称变为 NULL 或变为 NOT NULL,或名称保持 NULL):

CREATE TABLE #deleted (Id int identity(1,1), name nvarchar(32));
CREATE TABLE #inserted(Id int identity(1,1), name nvarchar(32));

INSERT #deleted (name) VALUES('bob'),('frank'), (NULL),  ('bla'),(NULL);
INSERT #inserted(name) VALUES('bob'),('franky'),('boop'),(NULL), (NULL);

-- not correct:
SELECT i.Id, 'Name', d.Name, i.Name
  FROM #deleted d, #inserted i
  WHERE d.Name <> i.Name 
    OR (d.Name IS NULL AND i.Name IS NOT NULL) 
    OR (d.Name IS NOT NULL AND i.Name IS NULL)

-- correct:
SELECT i.Id, 'Name', d.Name, i.Name
  FROM #deleted AS d
  INNER JOIN #inserted AS i
  ON d.Id = i.Id
  WHERE d.Name <> i.Name 
    OR (d.Name IS NULL AND i.Name IS NOT NULL) 
    OR (d.Name IS NOT NULL AND i.Name IS NULL);

您需要加入触发器才能看起来更像后者。

此外,我建议始终在逻辑块周围使用BEGIN/END。

IF something
BEGIN
    do a bunch of stuff
END

...可能会打字多一些,但对于读者来说,它比...更具可预测性和直观性。

IF something
    do a bunch of stuff

【讨论】:

  • 使用内连接有效!对于这种情况下的其他任何人,我最终也将插入、更新和删除分离到他们自己的触发器中。使代码更具可读性和简洁性; if 语句检查和奇怪的解决方法要少得多。在这种情况下,我会向其他人推荐这种方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多