Jak użyć COLUMNS_UPDATED, aby sprawdzić, czy któraś z niektórych kolumn jest zaktualizowana?

13

Mam tabelę z 42 kolumnami i wyzwalaczem, który powinien zrobić coś, gdy 38 z tych kolumn zostanie zaktualizowanych. Więc muszę pominąć logikę, jeśli pozostałe 4 kolumny zostaną zmienione.

Mogę użyć funkcji UPDATE () i stworzyć jeden duży IFwarunek, ale wolę zrobić coś krótszego. Za pomocą COLUMNS_UPDATED mogę sprawdzić, czy wszystkie niektóre kolumny są zaktualizowane?

Na przykład sprawdzenie, czy kolumny 3, 5 i 9 są zaktualizowane:

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

wprowadź opis zdjęcia tutaj

Zatem wartość 20dla kolumny 3i 5oraz wartość 1dla kolumny, 9ponieważ jest ona ustawiona w pierwszym bicie drugiego bajtu. Jeśli zmienię na to instrukcję OR, sprawdzi, czy kolumny 3i / 5lub kolumna 9są / są aktualizowane?

Jak zastosować ORlogikę w kontekście jednego bajtu?

gotqn
źródło
7
Czy chcesz wiedzieć, czy te kolumny są wymienione na SETliście, czy też wartości rzeczywiście się zmieniły? Zarówno UPDATEi COLUMNS_UPDATED()tylko ci powiem pierwszy. Jeśli chcesz wiedzieć, czy wartości rzeczywiście się zmieniły, musisz dokonać właściwego porównania insertedi deleted.
Aaron Bertrand
Zamiast używać SUBSTRINGdo dzielenia zwracanej wartości COLUMNS_UPDATED(), należy użyć porównania bitowego, jak pokazano w dokumentacji . Uwaga: jeśli w jakikolwiek sposób zmienisz tabelę, kolejność wartości zwracanych przez COLUMNS_UPDATED()zmieni się.
Max Vernon,
Jak nawiązywał do @AaronBertrand, jeśli chcesz zobaczyć wartości, które zostały zmienione, nawet jeśli nie zostały wyraźnie zaktualizowane za pomocą instrukcji SETlub UPDATE, możesz przyjrzeć się za pomocą CHECKSUM()lub BINARY_CHECKSUM(), a nawet HASHBYTES()nad danymi kolumnami.
Max Vernon,

Odpowiedzi:

18

Możesz użyć CHECKSUM()jako dość prostej metodologii porównywania rzeczywistych wartości, aby sprawdzić, czy zostały one zmienione. CHECKSUM()wygeneruje sumę kontrolną na liście przekazanych wartości, których liczba i typ są nieokreślone. Uważaj, istnieje niewielka szansa, że ​​porównanie sum kontrolnych takich jak ta spowoduje fałszywe negatywy. Jeśli nie możesz sobie z tym poradzić, możesz HASHBYTESzamiast tego użyć 1 .

W poniższym przykładzie użyto AFTER UPDATEwyzwalacza, aby zachować historię zmian wprowadzonych w TriggerTesttabeli tylko w przypadku zmiany jednej z wartości w kolumnach Data1 lub Data2 . W przypadku Data3zmian nie są podejmowane żadne działania.

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

wprowadź opis zdjęcia tutaj

Jeśli nalegasz na użycie funkcji COLUMNS_UPDATED () , nie powinieneś sztywno kodować wartości porządkowej danych kolumn, ponieważ definicja tabeli może się zmienić, co może unieważnić wartości zakodowane na stałe . Możesz obliczyć, jaka powinna być wartość w czasie wykonywania, korzystając z tabel systemowych. Należy pamiętać, że COLUMNS_UPDATED()funkcja zwraca wartość true dla danego bitu kolumny, jeśli kolumna jest modyfikowana w DOWOLNYM wierszu, na który wpływa UPDATE TABLEinstrukcja.

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;

wprowadź opis zdjęcia tutaj

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

wprowadź opis zdjęcia tutaj

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

wprowadź opis zdjęcia tutaj

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

wprowadź opis zdjęcia tutaj

To demo wstawia wiersze do tabeli historii, których być może nie należy wstawiać. W wierszach Data1zaktualizowano kolumnę dla niektórych wierszy i Data3zaktualizowano kolumnę dla niektórych wierszy. Ponieważ jest to pojedyncza instrukcja, wszystkie wiersze są przetwarzane przez pojedyncze przejście przez wyzwalacz. Ponieważ niektóre wiersze zostały Data1zaktualizowane, co stanowi część COLUMNS_UPDATED()porównania, wszystkie wiersze widoczne dla wyzwalacza są wstawiane do TriggerHistorytabeli. Jeśli jest to „niepoprawne” w twoim scenariuszu, być może będziesz musiał obsługiwać każdy wiersz osobno, używając kursora.

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;

TriggerResultTabela ma teraz jakieś potencjalnie mylących wiersze, które wyglądają tak, jakby nie należą, ponieważ pokazują one absolutnie żadnych zmian (w dwóch kolumnach w tej tabeli). W drugim zestawie wierszy na poniższym obrazku TriggerTestID 7 jest jedynym, który wygląda na zmodyfikowany. W pozostałych wierszach Data3zaktualizowano tylko kolumnę; jednak ponieważ zaktualizowano jeden wiersz w partii Data1, wszystkie wiersze są wstawiane do TriggerResulttabeli.

wprowadź opis zdjęcia tutaj

Alternatywnie, jako @AaronBertrand i @srutzky wskazał, można przeprowadzić porównanie rzeczywistych danych w insertedi deletedwirtualnych stołach. Ponieważ struktura obu tabel jest identyczna, możesz użyć EXCEPTklauzuli w wyzwalaczu, aby przechwycić wiersze, w których zmieniły się precyzyjne kolumny:

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 - patrz /programming/297960/hash-collision-what-are-the-ances szanse na omówienie znikomej szansy, że obliczenia HASHBYTES mogą również powodować kolizje. Preshing ma również przyzwoitą analizę tego problemu.

Max Vernon
źródło
2
To dobra informacja, ale „Jeśli nie możesz sobie z tym poradzić, możesz użyć HASHBYTESzamiast tego”. wprowadza w błąd. To prawda, że prawdopodobieństwo wystąpienia fałszywych negatywów HASHBYTESjest mniejsze niż CHECKSUM(prawdopodobieństwo zależy od wielkości zastosowanego algorytmu), ale nie można tego wykluczyć. Każda funkcja haszująca zawsze może potencjalnie powodować kolizje, ponieważ jest prawdopodobne, że zmniejszy ilość informacji. Jedynym sposobem, aby mieć pewność bez zmian jest porównanie INSERTEDi DELETEDtabele, i za pomocą _BIN2sortowania, jeśli jest to ciąg danych. Porównywanie skrótów daje pewność różnic.
Solomon Rutzky
2
@srutzky Jeśli będziemy się martwić kolizjami, podajmy również prawdopodobieństwo ich wystąpienia. stackoverflow.com/questions/297960/…
Dave
1
@Dave Nie mówię, nie używaj skrótów: używaj ich do identyfikowania elementów, które się zmieniły. Chodzi mi o to, że ponieważ prawdopodobieństwo> 0%, należy raczej stwierdzić niż sugerować, że jest gwarantowane (obecne sformułowanie, które zacytowałem), aby czytelnicy lepiej to zrozumieli. Tak, prawdopodobieństwo kolizji jest bardzo, bardzo małe, ale nie zerowe, i zależy od wielkości danych źródłowych. Jeśli muszę zagwarantować, że dwie wartości są takie same, spędzę kilka dodatkowych cykli procesora, aby to sprawdzić. W zależności od wielkości skrótu może nie być dużej różnicy między skrótem a porównaniem BIN2, więc wybierz 100% dokładny.
Solomon Rutzky
1
Dziękujemy za wpisanie tego przypisu (+1). Osobiście użyłbym zasobu innego niż ta konkretna odpowiedź, ponieważ jest to zbyt uproszczone. Istnieją dwa problemy: 1) wraz ze wzrostem wielkości wartości źródłowej rośnie prawdopodobieństwo. Wczoraj przeczytałem kilka postów na SO i innych stronach, a jedna osoba używająca tego na zdjęciach zgłosiła kolizje po 25 000 wpisów, a 2) prawdopodobieństwo jest takie, że względne ryzyko nie ma nic do powiedzenia, że ​​ktoś używający skrótu nie będzie wpaść w kolizje kilka razy w 10 000 wpisów. Szansa = szczęście. Można polegać, jeśli wiesz, że to szczęście ;-).
Solomon Rutzky