【问题标题】:Update function in TSQL triggerTSQL 触发器中的更新函数
【发布时间】:2010-11-17 22:56:59
【问题描述】:

我有一个关于 TSQL 函数更新的问题。例如,我有一个带有字段名称的表。如果我在更新后触发器中检查字段名称是否已更改,如下所示:

  if Update(Name)
  Begin
    -- process
  End

即使名称未更改,更新仍会返回 TRUE 吗?以下更新语句将使用相同的值对其进行更新:

  SELECT @v_Name = Name From MyTable Where Id = 1;
  Update MyTable Set Name = @v_Name where Id = 1;

如果即使 Name 的值没有改变,Update() 也返回 TRUE,我是否必须比较插入和删除的虚拟表中的值来确定值是否真的改变了?

顺便说一下,insert 和delete 是虚拟表,如果一条TSQL INSERT 或UPDATE 语句更改多行数据,它们可能包含多行数据。如果有多个记录,插入和删除的虚拟表中的行数是否相同,Update(Name)为TRUE的真正含义是什么?这是否意味着至少改变了一个?还是Update(Name)表示Name的字段已经被Update语句设置了,不管值是否改变?

我使用的 SQL 服务器是 Microsoft SQL 2005。

【问题讨论】:

    标签: sql sql-server sql-server-2005 tsql


    【解决方案1】:

    我认为下面的代码比上面的例子更好,因为它以简洁有效的方式只关注你想要检查的列。

    它确定一个值是否仅在指定的列中发生了变化。与其他解决方案相比,我没有研究它的性能,但它在我的数据库中运行良好。

    它使用 EXCEPT 集合运算符返回左侧查询中未在右侧查询中找到的任何行。此代码可用于 INSERT 和 UPDATE 触发器。

    “PrimaryKeyID”列是表的主键(可以是多个列),是启用两个集合之间的匹配所必需的。

    -- Only do trigger logic if specific field values change.
    IF EXISTS(SELECT  PrimaryKeyID
                    ,Column1
                    ,Column7
                    ,Column10
              FROM inserted
              EXCEPT
              SELECT PrimaryKeyID
                    ,Column1
                    ,Column7
                    ,Column10
              FROM deleted )    -- Tests for modifications to fields that we are interested in
    BEGIN
              -- Put code here that does the work in the trigger
    
    END
    

    如果你想在后续的触发逻辑中使用更改的行,我通常会将 EXCEPT 查询的结果放入一个表变量中,以便以后引用。

    我希望这很有趣:-)

    【讨论】:

      【解决方案2】:

      触发器很棘手,您在创建触发器时需要大量思考。每个 UPDATE 语句触发一次触发器。如果该 UPDATE 语句更新多行,则触发器仍只会触发一次。当 UPDATE 语句中包含该列时,UPDATE() 函数对该列返回 true。该功能允许您在更新语句中甚至不包含该列时回避 SQL 逻辑,从而有助于提高触发器的效率。它不会告诉您给定行中列的值是否发生了变化。

      这是一个示例表...

      CREATE TABLE tblSample
      (
          SampleID INT PRIMARY KEY,
          SampleName VARCHAR(10),
          SampleNameLastChangedDateTime DATETIME,
          Parent_SampleID INT
      )
      

      如果对这个表使用了以下 SQL:

      UPDATE tblSample SET SampleName = 'hello'
      

      ..并且 AFTER INSERT, UPDATE 触发器生效,这个特定的 SQL 语句将始终按如下方式评估 UPDATE 函数...

      IF UPDATE(SampleName) --aways evaluates to TRUE
      IF UPDATE(SampleID)  --aways evaluates to FALSE
      IF UPDATE(Parent_SampleID) --aways evaluates to FALSE
      

      请注意,无论 SampleName 之前的值是什么,该 SQL 语句的 UPDATE(SampleName) 始终为真。它返回 true 是因为 UPDATE 语句在该子句的 SET 部分中包含 SampleName 列,而不是基于之前或之后的值。 UPDATE() 函数不会确定值是否已更改。如果您想根据值是否更改来执行操作,您将需要使用 SQL 并比较插入和删除的行。

      以下是使最后更新的列保持同步的方法:

      --/*
      IF OBJECT_ID('dbo.tgr_tblSample_InsertUpdate', 'TR') IS NOT NULL 
        DROP TRIGGER dbo.tgr_tblSample_InsertUpdate
      GO
      --*/
      
      CREATE TRIGGER dbo.tgr_tblSample_InsertUpdate ON dbo.tblSample
        AFTER INSERT, UPDATE 
      AS
      BEGIN --Trigger
      
        IF UPDATE(SampleName)  
          BEGIN
            UPDATE tblSample SET
            SampleNameLastChangedDateTime = CURRENT_TIMESTAMP
            WHERE
              SampleID IN (SELECT Inserted.SampleID 
                     FROM Inserted LEFT JOIN Deleted ON Inserted.SampleID = Deleted.SampleID
                     WHERE COALESCE(Inserted.SampleName, '') <> COALESCE(Deleted.SampleName, ''))
          END
      
      END --Trigger
      

      判断行是否被更新的逻辑在上面的 WHERE 子句中。这是您需要做的真正检查。我的逻辑是使用 COALESCE 来处理 NULL 值和 INSERTS。

      ...
      WHERE
        SampleID IN (SELECT Inserted.SampleID 
                     FROM Inserted LEFT JOIN Deleted ON Inserted.SampleID = Deleted.SampleID
                     WHERE COALESCE(Inserted.SampleName, '') <> COALESCE(Deleted.SampleName, ''))
      

      请注意,IF UPDATE() 检查用于帮助提高触发器在 SampleName 列未更新时的效率。例如,如果一条 SQL 语句更新了 Parent_SampleID 列,那么当它不需要运行时,IF UPDATE(SampleName) 检查将有助于避开该 IF 语句中更复杂的逻辑。考虑在适当的时候使用 UPDATE(),但不要出于错误的原因。

      还要意识到,根据您的架构,UPDATE 功能可能对您没有用处。如果您的代码体系结构使用的中间层总是在保存对象时使用业务对象中的值更新表行中的所有列,则触发器中的 UPDATE() 函数将变得无用。在这种情况下,您的代码可能总是使用中间层发出的每个 UPDATE 语句来更新所有列。在这种情况下,当您的业务对象被保存时,UPDATE(columnname) 函数将始终评估为 true,因为所有列名始终包含在更新语句中。在这种情况下,在触发器中使用 UPDATE() 并没有帮助,而且在大多数情况下只会在该触发器中产生额外的开销。

      这里有一些 SQL 可以使用上面的触发器:

      INSERT INTO tblSample
      (
        SampleID,
        SampleName
      )
      SELECT 1, 'One'
      UNION SELECT 2, 'Two'
      UNION SELECT 3, 'Three'
      
      GO
      SELECT SampleID, SampleName, SampleNameLastChangedDateTime FROM tblSample
      
      /*
      SampleID  SampleName SampleNameLastChangedDateTime
      ----------- ---------- -----------------------------
      1       One    2010-10-27 14:52:42.567
      2       Two    2010-10-27 14:52:42.567
      3       Three  2010-10-27 14:52:42.567
      */
      
      GO
      
      INSERT INTO tblSample
      (
        SampleID,
        SampleName
      )
      SELECT 4, 'Foo'
      UNION SELECT 5, 'Five'
      
      GO
      SELECT SampleID, SampleName, SampleNameLastChangedDateTime FROM tblSample
      /*
      SampleID  SampleName SampleNameLastChangedDateTime
      ----------- ---------- -----------------------------
      1       One    2010-10-27 14:52:42.567
      2       Two    2010-10-27 14:52:42.567
      3       Three  2010-10-27 14:52:42.567
      4       Foo    2010-10-27 14:52:42.587
      5       Five   2010-10-27 14:52:42.587
      */
      
      GO
      
      UPDATE tblSample SET SampleName = 'Foo' 
      
      SELECT SampleID, SampleName, SampleNameLastChangedDateTime FROM tblSample 
      /*
      SampleID  SampleName SampleNameLastChangedDateTime
      ----------- ---------- -----------------------------
      1       Foo    2010-10-27 14:52:42.657
      2       Foo    2010-10-27 14:52:42.657
      3       Foo    2010-10-27 14:52:42.657
      4       Foo    2010-10-27 14:52:42.587
      5       Foo    2010-10-27 14:52:42.657
      */
      GO
      
      UPDATE tblSample SET SampleName = 'Not Prime' WHERE SampleID IN (1,4)
      
      SELECT SampleID, SampleName, SampleNameLastChangedDateTime FROM tblSample
      /*
      SampleID  SampleName SampleNameLastChangedDateTime
      ----------- ---------- -----------------------------
      1       Not Prime  2010-10-27 14:52:42.680
      2       Foo        2010-10-27 14:52:42.657
      3       Foo        2010-10-27 14:52:42.657
      4       Not Prime  2010-10-27 14:52:42.680
      5       Foo        2010-10-27 14:52:42.657
      */
      
      --Clean up...
      DROP TRIGGER dbo.tgr_tblSample_InsertUpdate
      DROP TABLE tblSample
      

      用户 GBN 提出以下建议:

      IF EXISTS (
          SELECT
              *
          FROM
              INSERTED I
              JOIN
              DELETED D ON I.key = D.key
          WHERE
              D.valuecol <> I.valuecol --watch for NULLs!
          )
         blah
      

      GBN 建议使用 IF (EXISTS( ...clause 并在存在已更改行的情况下将逻辑放入该 IF 语句中是可行的。该方法将对触发器中包含的所有行触发,即使只有一些行实际上已更改(这可能适合您的解决方案,但如果您只想对值更改的行执行某些操作,则可能不合适。)如果您需要对发生实际更改的行执行某些操作,您需要他提供的 SQL 中的不同逻辑。

      在我上面的示例中,当发出 UPDATE tblSample SET SampleName = 'Foo' 语句并且第四行已经是 'foo' 时,使用 GBN 的方法更新“最后更改的日期时间”列也会更新第四行,在这种情况下不合适。

      【讨论】:

        【解决方案3】:

        UPDATE() 可以为真,即使它的值相同。我个人不会依赖它,会比较价值。

        其次,DELETEDINSERTED 的行数相同。

        Update() 函数不是针对每行,而是针对所有行。不使用它的另一个原因。

        More here in MSDN,不过确实有点稀疏。

        评论后:

        IF EXISTS (
            SELECT
                *
            FROM
                INSERTED I
                JOIN
                DELETED D ON I.key = D.key
            WHERE
                D.valuecol <> I.valuecol --watch for NULLs!
            )
           blah
        

        【讨论】:

        • 谢谢。 SQL Server 提供的任何其他选项可用于找出更改?还是我必须遍历每一行才能找出变化?
        • 只在键上加入2个表,不需要循环
        • 那应该是deleted**inserted** 虚拟表,对吧?我从未听说过“更新”的虚拟表
        • 为了迂腐,deletedinserted 表仅在执行更新或没有行受到影响时(在删除或插入中)具有相同的行数.
        【解决方案4】:

        我同意确定列值是否实际更改(而不是使用相同值更新)的最佳方法是比较已删除和插入的伪表中的列值。但是,如果您想检查多个列,这可能会很痛苦。

        这是我在维护的一些代码中遇到的一个技巧(不知道原作者): 使用带有 HAVING 子句的 UNION 和 GROUP BY 来确定哪些列已更改。

        例如,在触发器中,获取已更改行的 ID:

        SELECT SampleID
        FROM 
            (
                SELECT SampleID, SampleName
                FROM deleted
        
                -- NOTE: UNION, not UNION ALL.  UNION by itself removes duplicate 
                --  rows.  UNION ALL includes duplicate rows.
                UNION 
        
                SELECT SampleID, SampleName
                FROM inserted
            ) x
        GROUP BY SampleID
        HAVING COUNT(*) > 1
        

        当您只检查单个列是否已更改时,这工作量太大。但是,如果您要检查 10 或 20 列,则 UNION 方法的工作量要比

        WHERE COALESCE(Inserted.Column1, '') <> COALESCE(Deleted.Column1, '')
            OR COALESCE(Inserted.Column2, '') <> COALESCE(Deleted.Column2, '')
            OR COALESCE(Inserted.Column3, '') <> COALESCE(Deleted.Column3, '')
            OR ...
        

        【讨论】:

          【解决方案5】:

          更新触发器将在 所有 更新语句上触发。受影响的行在“已插入”和“已删除”表的触发器中可用。您可以通过比较两个表中的 PK 列来比较旧值和新值(如果您有 PK)。 actual 表保持不变,直到触发器完成执行。

          【讨论】:

          • 在触发器完成之前说“实际”表是不正确的。如果触发器是 AFTER 触发器,则表将在触发器被调用时更新。外部环境只有在触发器完成后才能看到更新。
          猜你喜欢
          • 2021-05-06
          • 2011-01-15
          • 1970-01-01
          • 2017-08-02
          • 2013-02-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-09-09
          相关资源
          最近更新 更多