SQL触发器中的更新功能

时间:2009-08-08 14:43:19

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

我对TSQL函数更新有疑问。例如,我有一个带有字段名称的表。如果我在After Update触发器中检查是否更改了字段名称,请执行以下操作:

  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;

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

顺便说一句,插入和删除的是虚拟表,如果一个TSQL INSERT或UPDATE语句更改了多行数据,它们可能包含多行数据。如果有多个记录,插入和删除的虚拟表中的行数是否相同,更新(名称)的真正含义是什么?这是否意味着至少有一个被改变了?或者更新(名称)是否表示Name的字段已由Update语句设置,无论该值是否已更改?

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

5 个答案:

答案 0 :(得分:33)

触发器很棘手,你需要在创建时大量思考。触发器会为每个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

请注意,对于此SQL语句,UPDATE(SampleName)始终为true,无论之前的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语句中。在这种情况下,在触发器中使用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(...子句并将逻辑放在IF语句中,如果存在已更改的行可能有效。即使只有部分行,这种方法也会触发包含在触发器中的所有行)行实际上已更改(这可能适合您的解决方案,但如果您只想对值更改的行执行某些操作,则可能也不合适。)如果您需要对发生实际更改的行执行某些操作,则在他提供的SQL中需要不同的逻辑。

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

答案 1 :(得分:20)

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

答案 2 :(得分:5)

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

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

例如,在触发器中,获取已更改行的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 ...

答案 3 :(得分:3)

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

它确定某个值是否仅在指定的列中发生了更改。与其他解决方案相比,我没有调查过它的性能,但它在我的数据库中运行良好。

它使用EXCEPT set运算符返回左查询中的任何行,这些行在右侧查询中也找不到。此代码可用于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查询的结果放入一个可以在以后引用的表变量中。

我希望这很有趣: - )

答案 4 :(得分:0)

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