Как использовать 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для столбца 3и 5, и значение 1для столбца, 9потому что оно установлено в первом бите второго байта. Если я изменю утверждение, ORоно будет проверять , обновляются ли столбцы 3и / 5или столбец 9?

Как можно применить ORлогику в контексте одного байта?


7
Хорошо, вы хотите знать, упомянуты ли эти столбцы в SETсписке, или значения действительно изменились? И то UPDATEи другое COLUMNS_UPDATED()скажу только первое. Если вы хотите узнать, изменились ли значения на самом деле, вам нужно сделать правильное сравнение insertedи deleted.
Аарон Бертран

Вместо того, SUBSTRINGчтобы использовать для разделения значения, возвращаемого формой COLUMNS_UPDATED(), вы должны использовать побитовое сравнение, как показано в документации . Помните, что если вы измените таблицу каким-либо образом, COLUMNS_UPDATED()изменится порядок значений, возвращаемых .
Макс Вернон

Как сказал @AaronBertrand, если вам нужно увидеть значения, которые были изменены, даже если они не были явно обновлены с помощью оператора SETили UPDATE, вы можете захотеть посмотреть на использование CHECKSUM()или BINARY_CHECKSUM(), или даже HASHBYTES()на рассматриваемые столбцы.
Макс Вернон

Ответы:


18

Вы можете использовать CHECKSUM()в качестве довольно простой методологии для сравнения фактических значений, чтобы увидеть, были ли они изменены. CHECKSUM()сгенерирует контрольную сумму по списку переданных значений, число и тип которых не определены. Осторожно, есть небольшой шанс, что сравнение контрольных сумм приведет к ложным отрицаниям. Если вы не можете справиться с этим, вы можете использовать HASHBYTESвместо 1 .

В приведенном ниже примере используется 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

введите описание изображения здесь

Если вы настаиваете на использовании функции COLUMNS_UPDATED () , вам не следует жестко кодировать порядковый номер рассматриваемых столбцов, так как определение таблицы может измениться, что может сделать недействительными жестко закодированные значения. Вы можете рассчитать, какое значение должно быть во время выполнения, используя системные таблицы. Имейте в виду, что COLUMNS_UPDATED()функция возвращает истину для заданного бита столбца, если столбец изменен в ЛЮБОЙ строке, затронутой 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 отметили, вы можете выполнить сравнение фактических данных в insertedи deletedвиртуальных таблицах. Поскольку структура обеих таблиц идентична, вы можете использовать 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 - см. Https://stackoverflow.com/questions/297960/hash-collision-what-are-the-chances, чтобы обсудить маловероятную вероятность того, что расчет HASHBYTES также может привести к коллизиям. У Прешинга также есть приличный анализ этой проблемы.


2
Это хорошая информация, но «Если вы не можете справиться с этим, вы можете использовать HASHBYTESвместо этого». вводит в заблуждение. Это правда , что HASHBYTESэто менее вероятно , чтобы иметь ложные негативы , чем CHECKSUM(правдоподобия варьирования по размеру используемого алгоритма), но она не может быть исключена. Любая хеширующая функция всегда может иметь коллизии, поскольку вполне вероятно, что информация будет сокращена. Единственный способ быть уверенным в каких - либо изменений , чтобы сравнить INSERTEDи DELETEDтаблиц, а также с помощью _BIN2сортировки , если это строка данных. Сравнение хэшей только дает уверенность в различиях.
Соломон Руцкий,

2
@srutzky Если мы будем беспокоиться о столкновениях, давайте также укажем вероятность их возникновения. stackoverflow.com/questions/297960/…
Дэйв

1
@ Дэйв Я не говорю, не используйте хэши: используйте их, чтобы идентифицировать элементы, которые изменились. Моя точка зрения заключается в том, что, поскольку вероятность> 0%, следует указать, а не подразумевать, что она гарантирована (нынешняя формулировка, которую я цитировал), чтобы читатели понимали ее лучше. Да, вероятность столкновения очень и очень мала, но не равна нулю и зависит от размера исходных данных. Если мне нужно гарантировать, что два значения совпадают, я потрачу несколько дополнительных циклов ЦП для проверки. В зависимости от размера хеша, разница между хешем и сравнением BIN2 может быть незначительной, поэтому выбирайте 100% точный.
Соломон Руцкий

1
Спасибо за добавление этой сноски (+1). Лично я бы использовал ресурс, отличный от этого конкретного ответа, так как он слишком упрощен. Есть две проблемы: 1) с увеличением размера исходного значения вероятность возрастает. Прошлой ночью я прочитал несколько сообщений на SO и других сайтах, и один человек, использующий это на изображениях, сообщил о столкновениях после 25 000 записей, и 2) вероятность - это просто относительный риск, что нечего сказать, что кто-то, использующий хеш, не будет столкнуться несколько раз в 10 000 записей. Шанс = удача. На это можно положиться, если вы знаете, что это удача ;-).
Соломон Руцкий
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.