Wie kann man mit COLUMNS_UPDATED prüfen, ob eine bestimmte Spalte aktualisiert wurde?


12

Ich habe eine Tabelle mit 42 Spalten und einen Trigger, der einige Dinge tun sollte, wenn 38 dieser Spalten aktualisiert werden. Ich muss also die Logik überspringen, wenn die restlichen 4 Spalten geändert werden.

Ich kann die UPDATE () - Funktion verwenden und eine große IFBedingung erstellen , bevorzuge jedoch eine kürzere. Mit COLUMNS_UPDATED kann ich überprüfen, ob alle bestimmten Spalten aktualisiert wurden?

Überprüfen Sie beispielsweise, ob die Spalten 3, 5 und 9 aktualisiert wurden:

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

Bildbeschreibung hier eingeben

Also Wert 20für Spalte 3und 5und Wert 1für Spalte, 9da er im ersten Bit des zweiten Bytes gesetzt ist. Wenn ich die Anweisung in ändere, ORwird überprüft, ob Spalten 3und / 5oder Spalten 9aktualisiert werden.

Wie kann man ORLogik im Kontext eines Bytes anwenden ?


7
Möchten Sie wissen, ob diese Spalten in der SETListe aufgeführt sind oder ob sich die Werte tatsächlich geändert haben? Beides UPDATEund COLUMNS_UPDATED()nur das erstere. Wenn Sie wissen möchten, ob sich die Werte tatsächlich geändert haben, müssen Sie einen ordnungsgemäßen Vergleich von insertedund durchführen deleted.
Aaron Bertrand

Anstatt SUBSTRINGdas zurückgegebene Wertformular zu teilen COLUMNS_UPDATED(), sollten Sie einen bitweisen Vergleich verwenden, wie in der Dokumentation gezeigt . Beachten Sie, dass sich die Reihenfolge der von zurückgegebenen Werte ändert, wenn Sie die Tabelle auf irgendeine Weise COLUMNS_UPDATED()ändern.
Max Vernon

Wenn Sie, wie @AaronBertrand andeutete, Werte sehen möchten, die geändert wurden, obwohl sie nicht explizit mit einer SEToder -Anweisung aktualisiert wurden UPDATE, sollten Sie sich die Verwendung von CHECKSUM()oder BINARY_CHECKSUM()oder sogar HASHBYTES()die betreffenden Spalten ansehen .
Max Vernon

Antworten:


17

Sie können CHECKSUM()eine relativ einfache Methode zum Vergleichen der tatsächlichen Werte verwenden, um festzustellen, ob sie geändert wurden. CHECKSUM()erzeugt eine Prüfsumme über eine Liste von übergebenen Werten, deren Anzahl und Typ unbestimmt sind. Achtung, es gibt eine kleine Chance, dass ein Vergleich von Prüfsummen zu falschen Negativen führt. Wenn Sie damit nicht umgehen können, können Sie HASHBYTESstattdessen 1 verwenden .

Im folgenden Beispiel wird ein AFTER UPDATETrigger verwendet, um den Verlauf der an der TriggerTestTabelle vorgenommenen Änderungen nur dann beizubehalten, wenn sich einer der Werte in der Spalte Data1 oder Data2 ändert. Bei Data3Änderungen wird keine Aktion ausgeführt.

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

Bildbeschreibung hier eingeben

Wenn Sie darauf bestehen, die Funktion COLUMNS_UPDATED () zu verwenden , sollten Sie den Ordinalwert der betreffenden Spalten nicht hartcodieren , da sich die Tabellendefinition ändern kann, wodurch hartcodierte Werte ungültig werden können. Anhand der Systemtabellen können Sie berechnen, wie hoch der Wert zur Laufzeit sein soll. Beachten Sie, dass die COLUMNS_UPDATED()Funktion für die gegebene Spalte Bit true zurück , wenn die Spalte in modifiziert wird jedem der betroffenen Zeile UPDATE TABLEAussage.

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;

Bildbeschreibung hier eingeben

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

Bildbeschreibung hier eingeben

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

Bildbeschreibung hier eingeben

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

Bildbeschreibung hier eingeben

Diese Demo fügt Zeilen in die Verlaufstabelle ein, die möglicherweise nicht eingefügt werden sollten. Die Reihen haben ihre hatten Data1Spalte für einige Zeilen aktualisiert und haben die hatten Data3Spalte für einige Zeilen aktualisiert. Da es sich um eine einzelne Anweisung handelt, werden alle Zeilen in einem Durchgang durch den Trigger verarbeitet. Da einige Zeilen Data1aktualisiert wurden, was Teil des COLUMNS_UPDATED()Vergleichs ist, werden alle vom Trigger angezeigten Zeilen in die TriggerHistoryTabelle eingefügt . Wenn dies für Ihr Szenario "falsch" ist, müssen Sie möglicherweise jede Zeile einzeln mit einem Cursor behandeln.

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;

Die TriggerResultTabelle enthält jetzt einige möglicherweise irreführende Zeilen, die so aussehen, als gehörten sie nicht dazu, da sie absolut keine Änderungen aufweisen (an den beiden Spalten in dieser Tabelle). In der zweiten Reihe von Zeilen im Bild unten ist TriggerTestID 7 die einzige, die so aussieht, als ob sie geändert wurde. In den anderen Zeilen wurde nur die Data3Spalte aktualisiert. Da jedoch die eine Zeile im Stapel Data1aktualisiert wurde, werden alle Zeilen in die TriggerResultTabelle eingefügt .

Bildbeschreibung hier eingeben

Alternativ können Sie, wie @AaronBertrand und @srutzky betonten, einen Vergleich der tatsächlichen Daten in der insertedund der deletedvirtuellen Tabelle durchführen . Da die Struktur beider Tabellen identisch ist, können Sie eine EXCEPTKlausel im Trigger verwenden, um Zeilen zu erfassen, in denen sich die genauen gewünschten Spalten geändert haben:

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 - siehe /programming/297960/hash-collision-what-are-the-chances für eine Diskussion der verschwindend geringen Wahrscheinlichkeit, dass die HASHBYTES-Berechnung auch zu Kollisionen führen kann. Preshing hat auch eine anständige Analyse dieses Problems.


2
Dies ist eine gute Info, aber "Wenn Sie damit nicht umgehen können, können Sie HASHBYTESstattdessen verwenden." ist irreführend. Es ist wahr, dass HASHBYTESes weniger wahrscheinlich ist , dass ein falsches Negativ vorliegt als CHECKSUM(Wahrscheinlichkeit variiert je nach Größe des verwendeten Algorithmus), aber es kann nicht ausgeschlossen werden. Jede Hash-Funktion kann immer zu Kollisionen führen, da es sich mit ziemlicher Wahrscheinlichkeit um reduzierte Informationen handelt. Der einzige Weg , um sicher zu gehen ohne Änderungen ist es, die zu vergleichen INSERTEDund DELETEDTabellen, und mit einer _BIN2Sortierungs wenn es String - Daten ist. Das Vergleichen von Hashes gibt nur Sicherheit für Unterschiede.
Solomon Rutzky

2
@srutzky Wenn wir uns Sorgen um Kollisionen machen wollen, geben wir auch die Wahrscheinlichkeit dafür an. stackoverflow.com/questions/297960/…
Dave

1
@ Dave Ich sage nicht, verwenden Sie keine Hashes: Verwenden Sie diese, um Elemente zu identifizieren, die sich geändert haben. Mein Punkt ist, da die Wahrscheinlichkeit> 0% ist, sollte nicht impliziert, dass es garantiert ist (der aktuelle Wortlaut, den ich zitierte), damit die Leser es besser verstehen. Ja, die Wahrscheinlichkeit einer Kollision ist sehr, sehr gering, aber nicht Null und variiert je nach Größe der Quelldaten. Wenn ich garantieren muss, dass zwei Werte gleich sind, werde ich einige zusätzliche CPU-Zyklen für die Überprüfung aufwenden. Abhängig von der Hash-Größe gibt es möglicherweise keinen großen Leistungsunterschied zwischen dem Hash und einem BIN2-Vergleich. Entscheiden Sie sich also für den 100% genauen.
Solomon Rutzky

1
Vielen Dank, dass Sie diese Fußnote (+1) eingegeben haben. Persönlich würde ich eine andere Ressource als diese bestimmte Antwort verwenden, da dies zu simpel ist. Es gibt zwei Probleme: 1) Wenn die Quellwertgrößen größer werden, steigt die Wahrscheinlichkeit. Ich habe letzte Nacht mehrere Posts auf SO und anderen Websites durchgelesen, und eine Person, die diese Posts auf Bildern verwendet, hat Kollisionen nach 25.000 Einträgen gemeldet in 10k Einträgen mehrmals auf Kollisionen stoßen. Zufall = Glück. Es ist in Ordnung, sich darauf zu verlassen, wenn Sie wissen, dass es Glück ist ;-).
Solomon Rutzky
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.