Як за допомогою 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 aluded до, якщо вам потрібно , щоб побачити значення , які були змінені , навіть якщо вони не були явно оновлені з використанням 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-зміни для обговорення зникаючого малого шансу, що розрахунок HASHBYTES також може спричинити зіткнення. Preshing також має гідний аналіз цієї проблеми.


2
Це гарна інформація, але "Якщо ви не можете з цим впоратися, можете скористатися HASHBYTESнатомість". вводить в оману. Це правда , що HASHBYTESце менш імовірно , щоб мати помилкові негативи , ніж CHECKSUM(правдоподібності варіювання за розміром використовуваного алгоритму), але вона не може бути виключена. Будь-яка функція хешування завжди матиме можливість зіткнення, оскільки цілком ймовірно, що це зменшена інформація. Єдиний спосіб бути впевненим у відсутності змін - це порівняння таблиць INSERTEDта DELETEDтаблиць та використання _BIN2зіставлення, якщо це рядкові дані. Порівняння хешей дає лише певність для відмінностей.
Соломон Руцький

2
@srutzky Якщо ми будемо турбуватися про зіткнення, давайте також викладемо ймовірність цього. stackoverflow.com/questions/297960/…
Дейв

1
@Dave Я не кажу, що не використовуйте хеші: використовуйте їх для визначення елементів, які змінилися. Моя думка, оскільки ймовірність> 0%, слід констатувати, а не мати на увазі, що це гарантовано (нинішня редакція, яку я цитував), щоб читачі зрозуміли це краще. Так, ймовірність зіткнення дуже, дуже мала, але не дорівнює нулю, і залежить від розміру вихідних даних. Якщо мені потрібно гарантувати, що два значення однакові, я проведу кілька додаткових циклів процесора, щоб перевірити. Залежно від розміру хешу, між хешем і BIN2 порівнянням може бути не дуже велика різниця в перфі, тому перейдіть на 100% точний.
Соломон Руцький

1
Дякуємо, що помістили цю виноску (+1). Особисто я би використав ресурс, відмінний від конкретної відповіді, оскільки він надто спрощений. Є два питання: 1) у міру збільшення розмірів вихідного значення збільшується ймовірність. Я прочитав кілька дописів на SO та інших сайтах минулої ночі, і одна людина, яка використовує це на зображеннях, повідомила про зіткнення після 25000 записів, і 2) ймовірність полягає лише в тому, що відносний ризик, нічого не можна сказати, що хтось, хто використовує хеш, не буде кілька разів зіткнетесь із записами 10 к. Шанс = удача. Добре покладатися на те, якщо ви знаєте, що це удача ;-).
Соломон Руцький
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.