Come utilizzare COLUMNS_UPDATED per verificare se una di determinate colonne viene aggiornata?


13

Ho una tabella con 42 colonne e un trigger che dovrebbe fare alcune cose quando 38 di queste colonne vengono aggiornate. Quindi, devo saltare la logica se le altre 4 colonne vengono modificate.

Posso usare la funzione UPDATE () e creare una grande IFcondizione, ma preferisco fare qualcosa di più breve. Utilizzando COLUMNS_UPDATED posso verificare se tutte alcune colonne sono state aggiornate?

Ad esempio, controllando se le colonne 3, 5 e 9 sono aggiornate:

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

inserisci qui la descrizione dell'immagine

Quindi, valore 20per colonna 3e 5e valore 1per colonna 9perché è impostato nel primo bit del secondo byte. Se cambio la dichiarazione ORsi verificherà se le colonne 3e 5o colonna 9viene / vengono aggiornati?

Come può applicare la ORlogica nel contesto di un byte?


7
Bene, vuoi sapere se quelle colonne sono menzionate SETnell'elenco o se i valori sono effettivamente cambiati? Entrambi UPDATEe COLUMNS_UPDATED()ti dico solo il primo. Se vuoi sapere se i valori sono effettivamente cambiati, dovrai fare un confronto adeguato di insertede deleted.
Aaron Bertrand

Invece di utilizzare SUBSTRINGper dividere il valore restituito dal modulo COLUMNS_UPDATED(), è necessario utilizzare un confronto bit a bit, come mostrato nella documentazione . Attenzione che se si modifica la tabella in qualche modo, l'ordine dei valori restituiti COLUMNS_UPDATED()cambierà.
Max Vernon,

Come accennato a @AaronBertrand, se hai bisogno di vedere valori che sono stati cambiati anche se non sono stati esplicitamente aggiornati usando un'istruzione SETo UPDATE, potresti voler guardare usando CHECKSUM()o BINARY_CHECKSUM(), o anche HASHBYTES()sopra le colonne in questione.
Max Vernon,

Risposte:


18

Puoi utilizzare CHECKSUM()una metodologia abbastanza semplice per confrontare i valori reali per vedere se sono stati modificati. CHECKSUM()genererà un checksum attraverso un elenco di valori passati, di cui il numero e il tipo sono indeterminati. Attenzione, c'è una piccola possibilità che il confronto di checksum come questo comporterà falsi negativi. Se non puoi gestirlo, puoi usare HASHBYTESinvece 1 .

L'esempio seguente utilizza un AFTER UPDATEtrigger per conservare una cronologia delle modifiche apportate alla TriggerTesttabella solo se uno dei valori nelle colonne Data1 o Data2 cambia. Se Data3cambia, non viene intrapresa alcuna azione.

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

inserisci qui la descrizione dell'immagine

Se si insiste sull'uso della funzione COLUMNS_UPDATED () , non è necessario codificare il valore ordinale delle colonne in questione, poiché la definizione della tabella potrebbe cambiare, il che potrebbe invalidare i valori codificati. È possibile calcolare quale valore dovrebbe essere in fase di esecuzione utilizzando le tabelle di sistema. Tenere presente che la COLUMNS_UPDATED()funzione restituisce true per il bit di colonna specificato se la colonna viene modificata in QUALSIASI riga interessata UPDATE TABLEdall'istruzione.

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;

inserisci qui la descrizione dell'immagine

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

inserisci qui la descrizione dell'immagine

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

inserisci qui la descrizione dell'immagine

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

inserisci qui la descrizione dell'immagine

Questa demo inserisce righe nella tabella della cronologia che forse non dovrebbero essere inserite. Le righe hanno avuto la Data1colonna aggiornata per alcune righe e la Data3colonna è stata aggiornata per alcune righe. Poiché si tratta di una singola istruzione, tutte le righe vengono elaborate da un singolo passaggio attraverso il trigger. Poiché alcune righe sono state Data1aggiornate, il che fa parte del COLUMNS_UPDATED()confronto, tutte le righe visualizzate dal trigger vengono inserite nella TriggerHistorytabella. Se questo è "errato" per il tuo scenario, potresti dover gestire ogni riga separatamente, usando un cursore.

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;

La TriggerResulttabella ora ha alcune righe potenzialmente fuorvianti che sembrano non appartenere poiché non mostrano assolutamente alcuna modifica (alle due colonne in quella tabella). Nel secondo set di righe nell'immagine seguente, TriggerTestID 7 è l'unico che sembra sia stato modificato. Le altre righe avevano solo la Data3colonna aggiornata; tuttavia poiché la prima riga nel batch è stata Data1aggiornata, tutte le righe vengono inserite nella TriggerResulttabella.

inserisci qui la descrizione dell'immagine

In alternativa, come sottolineato da @AaronBertrand e @srutzky, è possibile eseguire un confronto dei dati effettivi nelle tabelle virtuali insertede deleted. Poiché la struttura di entrambe le tabelle è identica, puoi utilizzare una EXCEPTclausola nel trigger per acquisire righe in cui sono cambiate le colonne precise a cui sei interessato:

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 - vedi /programming/297960/hash-collision-what-are-the-chances per una discussione sulle possibilità minime che il calcolo di HASHBYTES possa anche causare collisioni. Anche il preshing ha un'analisi decente di questo problema.


2
Questa è una buona informazione, ma "Se non puoi occupartene, puoi usare HASHBYTESinvece". è fuorviante. È vero che HASHBYTESè meno probabile che abbia falsi negativi rispetto CHECKSUM(probabilità che varia in base alle dimensioni dell'algoritmo utilizzato), ma non può essere escluso. Qualsiasi funzione di hashing avrà sempre il potenziale di avere collisioni poiché è molto probabile che si tratti di informazioni ridotte. L'unico modo per essere certi di nessuna modifica è quello di confrontare le INSERTEDe DELETEDtavoli, e utilizzando una _BIN2collazione, se si tratta di dati di stringa. Il confronto degli hash fornisce solo certezza per le differenze.
Solomon Rutzky,

2
@srutzky Se dovremo preoccuparci delle collisioni, dichiariamo anche la loro probabilità. stackoverflow.com/questions/297960/…
Dave

1
@Dave Non sto dicendo di non usare gli hash: usali per identificare gli elementi che sono cambiati. Il mio punto è che, poiché la probabilità è> 0%, dovrebbe essere dichiarato piuttosto che implicito che è garantito (l'attuale formulazione che ho citato) in modo che i lettori lo capiscano meglio. Sì, la probabilità di una collisione è molto, molto piccola, ma non zero, e varia in base alla dimensione dei dati di origine. Se devo garantire che due valori siano uguali, spenderò alcuni cicli di CPU aggiuntivi per verificare. A seconda della dimensione dell'hash, potrebbe non esserci molta differenza perf tra l'hash e un confronto BIN2, quindi scegli quello accurato al 100%.
Solomon Rutzky,

1
Grazie per aver inserito la nota (+1). Personalmente, userei una risorsa diversa da quella particolare risposta in quanto eccessivamente semplicistica. Esistono due problemi: 1) man mano che le dimensioni del valore sorgente aumentano, la probabilità aumenta. Ho letto diversi post su SO e altri siti la scorsa notte, e una persona che lo utilizzava sulle immagini ha riportato collisioni dopo 25.000 voci e 2) la probabilità è proprio questo, il rischio relativo, non c'è niente da dire che qualcuno che usa un hash non lo farà inciampare alcune volte in 10k voci. Probabilità = fortuna. Va bene fare affidamento se si è consapevoli che si tratta di fortuna ;-).
Solomon Rutzky,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.