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


13

我有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';

在此处输入图片说明

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

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


7
好吧,您是否想知道SET列表中是否提到了这些列,或者值实际上是否已更改?双方UPDATECOLUMNS_UPDATED()只能告诉你前者。如果你想知道,如果实际值发生变化,你需要做的正确的比较inserteddeleted
亚伦·伯特兰

不应使用SUBSTRING拆分返回的值形式COLUMNS_UPDATED(),而应使用按位比较,如文档所示。请注意,如果以任何方式更改表,则返回的值的顺序COLUMNS_UPDATED()都会改变。
Max Vernon

正如@AaronBertrand所坚持的那样,如果即使没有使用SETor UPDATE语句来显式更新值,也需要查看已更改的值,则可能要使用CHECKSUM()or BINARY_CHECKSUM(),甚至HASHBYTES()是所讨论的列。
Max Vernon

Answers:


18

您可以使用CHECKSUM()一种非常简单的方法来比较实际值,以查看它们是否已更改。 CHECKSUM()会在传入值列表中生成一个校验和,其值和类型不确定。当心,比较这样的校验和的机会很小,这将导致假阴性。如果您无法处理,则可以HASHBYTES改用1

以下示例仅在列中的任何一个值更改时,才使用AFTER UPDATE触发器保留对TriggerTest表所做的修改的历史记录。如果更改,则不采取任何措施。Data1 Data2Data3

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

在此处输入图片说明

如果您坚持使用COLUMNS_UPDATED()函数,则由于表定义可能会更改,这可能会使硬编码的值无效,因此不应硬编码所讨论的列的序数值。您可以使用系统表计算运行时的值。请注意,COLUMNS_UPDATED()如果在受该语句影响的ANY行中修改了该列,则该函数将为给定的列位返回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;

在此处输入图片说明

--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;

在此处输入图片说明

--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

在此处输入图片说明

--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;

在此处输入图片说明

此演示将在历史记录表中插入可能不应该插入的行。这些行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;

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

1-请参阅/programming/297960/hash-collision-what-are-the-chances,以讨论HASHBYTES计算也可能导致碰撞的可能性极小。 Preshing也对此问题进行了不错的分析。


2
这是个很好的信息,但是“如果您不能解决这个问题,则可以HASHBYTES改用。” 有误导性。这是真的,HASHBYTES不太可能有假阴性比CHECKSUM(所使用的算法的可能性大小而有所不同),但也不能排除。任何哈希函数都将始终具有冲突的可能性,因为它很可能会减少信息量。确定不变的唯一方法是比较INSERTEDDELETED表,_BIN2如果它是字符串数据,则使用归类。比较哈希只能确定差异。
所罗门·鲁兹基

2
@srutzky如果我们要担心碰撞,我们也要说明其可能性。stackoverflow.com/questions/297960/…–
戴夫

1
@戴夫我不是说不要使用哈希:使用它们来识别已更改的项目。我的观点是,由于可能性> 0%,因此应该声明而不是暗示它是有保证的(我引用的当前措辞),以便读者更好地理解它。是的,发生冲突的可能性非常非常小,但不为零,并且随源数据的大小而变化。如果我需要保证两个值相同,则将花费一些额外的CPU周期进行检查。根据哈希大小,哈希和BIN2比较之间可能不会有太多的性能差异,因此请选择100%准确的哈希。
所罗门·鲁兹基

1
感谢您添加该脚注(+1)。就我个人而言,我会使用不同于该特定答案的其他资源,因为它过于简单。有两个问题:1)随着源值大小变大,概率也增加。昨晚我在SO和其他网站上通读了几篇文章,一个人在图片上使用此工具报告了25,000个条目后发生了碰撞,并且2)概率仅仅是相对的风险,没有什么可说的是使用哈希的人不会在10k条目中发生几次碰撞。机会=运气。如果您知道运气不错,可以依靠它;-)。
所罗门·鲁兹基
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.