web-dev-qa-db-fra.com

Comment utiliser COLUMNS_UPDATED pour vérifier si certaines colonnes sont mises à jour?

J'ai une table avec 42 colonnes et un déclencheur qui devrait faire des choses lorsque 38 de ces colonnes sont mises à jour. Donc, je dois sauter la logique si les 4 autres colonnes sont modifiées.

Je peux utiliser la fonction PDATE () et créer une grande condition IF, mais je préfère faire quelque chose de plus court. Utilisation de COLUMNS_UPDATED Je peux vérifier si toutes certaines colonnes sont mises à jour?

Par exemple, vérifier si les colonnes 3, 5 et 9 sont mises à jour:

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

enter image description here

Donc, valeur 20 pour la colonne 3 et 5 et valeur 1 pour la colonne 9 car il est défini dans le premier bit du deuxième octet. Si je change l'instruction en OR, elle vérifiera si les colonnes 3 et 5 ou colonne 9 est/sont mis à jour?

Comment appliquer la logique OR dans le contexte d'un octet?

13
gotqn

Vous pouvez utiliser CHECKSUM() comme une méthodologie assez simple pour comparer les valeurs réelles pour voir si elles ont été modifiées. CHECKSUM() générera une somme de contrôle sur une liste de valeurs transmises, dont le nombre et le type sont indéterminés. Attention, il y a peu de chance de comparer des sommes de contrôle comme celle-ci entraînera de faux négatifs. Si vous ne pouvez pas gérer cela, vous pouvez utiliser HASHBYTES à la place1.

L'exemple ci-dessous utilise un déclencheur AFTER UPDATE Pour conserver un historique des modifications apportées à la table TriggerTest uniquement si l'une des valeurs du Data1 ou Les colonnes Data2 changent. Si Data3 Change, aucune action n'est entreprise.

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

enter image description here

Si vous insistez pour utiliser la fonction COLUMNS_UPDATED () , vous ne devez pas coder en dur la valeur ordinale des colonnes en question, car la définition de la table peut changer, ce qui peut invalider la valeur codée en dur ( s). Vous pouvez calculer la valeur à exécuter à l'aide des tables système. Sachez que la fonction COLUMNS_UPDATED() renvoie true pour le bit de colonne donné si la colonne est modifiée dans [~ # ~] any [~ # ~] ligne affectée par l'instruction 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;

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

Cette démo insère des lignes dans la table d'historique qui ne devraient peut-être pas être insérées. Les lignes ont vu leur colonne Data1 Mise à jour pour certaines lignes et la colonne Data3 A été mise à jour pour certaines lignes. Puisqu'il s'agit d'une instruction unique, toutes les lignes sont traitées par un seul passage à travers le déclencheur. Comme certaines lignes ont mis à jour Data1, Qui fait partie de la comparaison COLUMNS_UPDATED(), toutes les lignes vues par le déclencheur sont insérées dans la table TriggerHistory. Si cela est "incorrect" pour votre scénario, vous devrez peut-être gérer chaque ligne séparément, à l'aide d'un curseur.

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 table TriggerResult a maintenant des lignes potentiellement trompeuses qui semblent ne pas appartenir puisqu'elles ne montrent absolument aucune modification (aux deux colonnes de cette table). Dans le deuxième ensemble de lignes de l'image ci-dessous, TriggerTestID 7 est le seul qui semble avoir été modifié. Les autres lignes n'avaient que la colonne Data3 Mise à jour; cependant, puisque la ligne d'une du lot avait mis à jour Data1, toutes des lignes sont insérées dans la table TriggerResult.

enter image description here

Alternativement, comme l'ont souligné @AaronBertrand et @srutzky, vous pouvez effectuer une comparaison des données réelles dans les tables virtuelles inserted et deleted. Étant donné que la structure des deux tables est identique, vous pouvez utiliser une clause EXCEPT dans le déclencheur pour capturer les lignes où les colonnes précises qui vous intéressent ont changé:

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 - voir https://stackoverflow.com/questions/297960/hash-collision-what-are-the-chances pour une analyse de la possibilité infiniment petite que le calcul HASHBYTES puisse également entraîner des collisions . Preshing a également une analyse décente de ce problème.

18
Max Vernon