如何使用 COLUMNS_UPDATED 检查某些列是否已更新?

got*_*tqn 14 trigger sql-server t-sql

我有一个包含 42 列的表和一个触发器,当这些列中的 38 列更新时,它应该做一些事情。因此,如果其余 4 列发生更改,我需要跳过逻辑。

我可以使用UPDATE()函数并创建一个大IF条件,但更喜欢做一些更短的事情。使用COLUMNS_UPDATED我可以检查是否所有某些列都更新了?

例如,检查第 3、5 和 9 列是否更新:

  IF 
  (
    (SUBSTRING(COLUMNS_UPDATED(),1,1) & 20 = 20)
     AND 
    (SUBSTRING(COLUMNS_UPDATED(),2,1) & 1 = 1) 
  )
    PRINT 'Columns 3, 5 and 9 updated';
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

因此,203和的5值,以及1列的值,9因为它设置在第二个字节的第一位。如果我将语句更改为OR它会检查列3和/5或列9是否更新?

如何OR在一个字节的上下文中应用逻辑?

Han*_*non 19

您可以使用CHECKSUM()一种相当简单的方法来比较实际值以查看它们是否已更改。 CHECKSUM()将在传入值的列表中生成校验和,其中的数量和类型是不确定的。请注意,像这样比较校验和的可能性很小,会导致误报。如果您无法处理,则可以使用1HASHBYTES代替。

下面的示例使用AFTER UPDATE触发器来保留对TriggerTest表所做修改的历史记录,仅当Data1 Data2列中的任何一个值发生更改时。如果Data3更改,则不执行任何操作。

USE tempdb;
IF COALESCE(OBJECT_ID('dbo.TriggerTest'), 0) <> 0
BEGIN
    DROP TABLE dbo.TriggerTest;
END
CREATE TABLE dbo.TriggerTest
(
    TriggerTestID INT NOT NULL
        CONSTRAINT PK_TriggerTest
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , Data1 VARCHAR(10) NULL
    , Data2 VARCHAR(10) NOT NULL
    , Data3 DATETIME NOT NULL
);

IF COALESCE(OBJECT_ID('dbo.TriggerResult'), 0) <> 0
BEGIN
    DROP TABLE dbo.TriggerResult;
END
CREATE TABLE dbo.TriggerResult
(
    TriggerTestID INT NOT NULL
    , Data1OldVal VARCHAR(10) NULL
    , Data1NewVal VARCHAR(10) NULL
    , Data2OldVal VARCHAR(10) NULL
    , Data2NewVal VARCHAR(10) NULL
);

GO
IF COALESCE(OBJECT_ID('dbo.TriggerTest_AfterUpdate'), 0) <> 0 
BEGIN
    DROP TRIGGER TriggerTest_AfterUpdate;
END
GO
CREATE TRIGGER TriggerTest_AfterUpdate
ON dbo.TriggerTest
AFTER UPDATE
AS 
BEGIN
    INSERT INTO TriggerResult
    (
        TriggerTestID
        , Data1OldVal
        , Data1NewVal
        , Data2OldVal
        , Data2NewVal
    )
    SELECT d.TriggerTestID
        , d.Data1
        , i.Data1
        , d.Data2
        , i.Data2
    FROM inserted i 
        LEFT JOIN deleted d ON i.TriggerTestID = d.TriggerTestID
    WHERE CHECKSUM(i.Data1, i.Data2) <> CHECKSUM(d.Data1, d.Data2);
END
GO

INSERT INTO dbo.TriggerTest (Data1, Data2, Data3)
VALUES ('blah', 'foo', GETDATE());

UPDATE dbo.TriggerTest 
SET Data1 = 'blah', Data2 = 'fee' 
WHERE TriggerTestID = 1;

SELECT *
FROM dbo.TriggerTest;

SELECT *
FROM dbo.TriggerResult
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

如果您坚持使用COLUMNS_UPDATED() 函数,则不应硬编码相关列的序号值,因为表定义可能会更改,这可能会使硬编码值无效。您可以使用系统表计算运行时的值。请注意,COLUMNS_UPDATED()如果在受语句影响的任何行中修改了列,则该函数将为给定的列位返回 true UPDATE TABLE

USE tempdb;
IF COALESCE(OBJECT_ID('dbo.TriggerTest'), 0) <> 0
BEGIN
    DROP TABLE dbo.TriggerTest;
END
CREATE TABLE dbo.TriggerTest
(
    TriggerTestID INT NOT NULL
        CONSTRAINT PK_TriggerTest
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , Data1 VARCHAR(10) NULL
    , Data2 VARCHAR(10) NOT NULL
    , Data3 DATETIME NOT NULL
);

IF COALESCE(OBJECT_ID('dbo.TriggerResult'), 0) <> 0
BEGIN
    DROP TABLE dbo.TriggerResult;
END
CREATE TABLE dbo.TriggerResult
(
    TriggerTestID INT NOT NULL
    , Data1OldVal VARCHAR(10) NULL
    , Data1NewVal VARCHAR(10) NULL
    , Data2OldVal VARCHAR(10) NULL
    , Data2NewVal VARCHAR(10) NULL
);

GO
IF COALESCE(OBJECT_ID('dbo.TriggerTest_AfterUpdate'), 0) <> 0 
BEGIN
    DROP TRIGGER TriggerTest_AfterUpdate;
END
GO
CREATE TRIGGER TriggerTest_AfterUpdate
ON dbo.TriggerTest
AFTER UPDATE
AS 
BEGIN
    DECLARE @ColumnOrdinalTotal INT = 0;

    SELECT @ColumnOrdinalTotal = @ColumnOrdinalTotal 
        + POWER (
                2 
                , COLUMNPROPERTY(t.object_id,c.name,'ColumnID') - 1
            )
    FROM sys.schemas s
        INNER JOIN sys.tables t ON s.schema_id = t.schema_id
        INNER JOIN sys.columns c ON t.object_id = c.object_id
    WHERE s.name = 'dbo'
        AND t.name = 'TriggerTest'
        AND c.name IN (
            'Data1'
            , 'Data2'
        );

    IF (COLUMNS_UPDATED() & @ColumnOrdinalTotal) > 0
    BEGIN
        INSERT INTO TriggerResult
        (
            TriggerTestID
            , Data1OldVal
            , Data1NewVal
            , Data2OldVal
            , Data2NewVal
        )
        SELECT d.TriggerTestID
            , d.Data1
            , i.Data1
            , d.Data2
            , i.Data2
        FROM inserted i 
            LEFT JOIN deleted d ON i.TriggerTestID = d.TriggerTestID;
    END
END
GO

--this won't result in rows being inserted into the history table
INSERT INTO dbo.TriggerTest (Data1, Data2, Data3)
VALUES ('blah', 'foo', GETDATE());

SELECT *
FROM dbo.TriggerResult;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

--this will insert rows into the history table
UPDATE dbo.TriggerTest 
SET Data1 = 'blah', Data2 = 'fee' 
WHERE TriggerTestID = 1;

SELECT *
FROM dbo.TriggerTest;

SELECT *
FROM dbo.TriggerResult;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

--this WON'T insert rows into the history table
UPDATE dbo.TriggerTest 
SET Data3 = GETDATE()
WHERE TriggerTestID = 1;

SELECT *
FROM dbo.TriggerTest;

SELECT *
FROM dbo.TriggerResult
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

--this will insert rows into the history table, even though only
--one of the columns was updated
UPDATE dbo.TriggerTest 
SET Data1 = 'blum' 
WHERE TriggerTestID = 1;

SELECT *
FROM dbo.TriggerTest;

SELECT *
FROM dbo.TriggerResult;
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

此演示将可能不应插入的行插入到历史记录表中。Data1某些行的行已更新其列,某些行的Data3列已更新。由于这是一个单一的语句,所有行都通过触发器进行一次处理。由于某些行已Data1更新,这是COLUMNS_UPDATED()比较的一部分,触发器看到的所有行都被插入到TriggerHistory表中。如果这对您的方案来说是“不正确的”,您可能需要使用游标分别处理每一行。

INSERT INTO dbo.TriggerTest (Data1, Data2, Data3)
SELECT TOP(10) LEFT(o.name, 10)
    , LEFT(o1.name, 10)
    , GETDATE()
FROM sys.objects o
    , sys.objects o1;

UPDATE dbo.TriggerTest 
SET Data1 = CASE WHEN TriggerTestID % 6 = 1 THEN Data2 ELSE Data1 END
    , Data3 = CASE WHEN TriggerTestID % 6 = 2 THEN GETDATE() ELSE Data3 END;

SELECT *
FROM dbo.TriggerTest;

SELECT *
FROM dbo.TriggerResult;
Run Code Online (Sandbox Code Playgroud)

TriggerResult表现在有一些潜在的误导性行,看起来它们不属于这些行,因为它们绝对没有显示任何更改(对该表中的两列)。在下图中的第二组行中,TriggerTestID 7 是唯一一个看起来像被修改的行。其他行只Data3更新了列;然而,由于在一个批次中的行已经Data1更新,所有的行插入TriggerResult表中。

在此处输入图片说明

或者,正如@AaronBertrand 和@srutzky 所指出的,您可以对表inserteddeleted虚拟表中的实际数据进行比较。由于两个表的结构相同,您可以EXCEPT在触发器中使用一个子句来捕获您感兴趣的精确列已更改的行:

IF COALESCE(OBJECT_ID('dbo.TriggerTest_AfterUpdate'), 0) <> 0 
BEGIN
    DROP TRIGGER TriggerTest_AfterUpdate;
END
GO
CREATE TRIGGER TriggerTest_AfterUpdate
ON dbo.TriggerTest
AFTER UPDATE
AS 
BEGIN
    ;WITH src AS
    (
        SELECT d.TriggerTestID
            , d.Data1
            , d.Data2
        FROM deleted d
        EXCEPT 
        SELECT i.TriggerTestID
            , i.Data1
            , i.Data2
        FROM inserted i
    )
    INSERT INTO dbo.TriggerResult 
    (
        TriggerTestID, 
        Data1OldVal, 
        Data1NewVal, 
        Data2OldVal, 
        Data2NewVal
    )
    SELECT i.TriggerTestID
        , d.Data1
        , i.Data1
        , d.Data2
        , i.Data2
    FROM inserted i 
        INNER JOIN deleted d ON i.TriggerTestID = d.TriggerTestID
END
GO
Run Code Online (Sandbox Code Playgroud)

1 - 请参阅/sf/ask/20857231/以讨论 HASHBYTES 计算也可能导致冲突的可能性很小。 Preshing对这个问题也有很好的分析。

  • 这是很好的信息,但是“如果您无法解决这个问题,您可以改用`HASHBYTES`。” 是误导。确实,`HASHBYTES` 比 `CHECKSUM` 出现假阴性的可能性小(可能性因所用算法的大小而异),但不能排除这种可能性。任何散列函数总是有可能发生冲突,因为它很可能是减少信息。确定**没有变化**的唯一方法是比较`INSERTED`和`DELETED`表,如果它是字符串数据,则使用`_BIN2`排序规则。比较哈希值只能确定差异。 (3认同)
  • @srutzky如果我们要担心碰撞,让我们也说明它的可能性。http://stackoverflow.com/questions/297960/hash-collision-what-are-the-chances (3认同)
  • @Dave 我不是说不要使用哈希:使用它们来识别已更改的项目。我的观点是,由于可能性 &gt; 0%,所以应该说明而不是暗示它是有保证的(我引用的当前措辞),以便读者更好地理解它。是的,碰撞的概率非常非常小,但不是零,并且因源数据的大小而异。如果我需要保证两个值相同,我会多花几个 CPU 周期来检查。根据散列大小,散列和 BIN2 比较之间可能没有太大的性能差异,所以选择 100% 准确的。 (2认同)
  • 感谢您加入该脚注(+1)。就个人而言,我会使用该特定答案以外的资源,因为它过于简单化。有两个问题:1)随着源值大小变大,概率增加。昨晚我通读了 SO 和其他网站上的几篇文章,一个在图像上使用它的人在 25,000 个条目后报告了冲突,并且 2)概率只是相对风险,没有什么可以说有人使用哈希不会在 10k 个条目中遇到几次冲突。机会=运气。如果您知道这是运气,则可以依靠它;-)。 (2认同)