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

13

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 ignorer la logique si les 4 autres colonnes sont modifiées.

Je peux utiliser la fonction UPDATE () et créer une grande IFcondition, 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';

entrez la description de l'image ici

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

Comment appliquer la ORlogique dans le contexte d'un octet?

gotqn
la source
7
Eh bien, voulez-vous savoir si ces colonnes sont mentionnées dans la SETliste ou si les valeurs ont réellement changé? Les deux UPDATEet COLUMNS_UPDATED()seulement vous dire l'ancien. Si vous voulez savoir si les valeurs ont réellement changé, vous devrez comparer correctement insertedet et deleted.
Aaron Bertrand
Au lieu d'utiliser SUBSTRINGpour fractionner la valeur renvoyée COLUMNS_UPDATED(), vous devez utiliser une comparaison au niveau du bit, comme indiqué dans la documentation . Sachez que si vous modifiez la table de quelque manière que ce soit, l'ordre des valeurs renvoyées par COLUMNS_UPDATED()changera.
Max Vernon
Comme @AaronBertrand y a fait allusion, si vous devez voir les valeurs qui ont été modifiées même si elles n'ont pas été explicitement mises à jour à l'aide d'une instruction SETor UPDATE, vous pouvez envisager d'utiliser à l'aide de CHECKSUM()ou BINARY_CHECKSUM(), ou même HASHBYTES()sur les colonnes en question.
Max Vernon

Réponses:

18

Vous pouvez utiliser CHECKSUM()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 une petite 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 à la HASHBYTESplace 1 .

L'exemple ci-dessous utilise un AFTER UPDATEdéclencheur pour conserver un historique des modifications apportées à la TriggerTesttable uniquement si l'une des valeurs des colonnes Data1 ou Data2 change. En cas de Data3changement, 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

entrez la description de l'image ici

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 les valeurs codées en dur. Vous pouvez calculer quelle devrait être la valeur lors de l'exécution à l'aide des tables système. N'oubliez pas que la COLUMNS_UPDATED()fonction renvoie true pour le bit de colonne donné si la colonne est modifiée dans N'IMPORTE QUELLE ligne affectée par l' UPDATE TABLEinstruction.

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;

entrez la description de l'image ici

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

entrez la description de l'image ici

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

entrez la description de l'image ici

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

entrez la description de l'image ici

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 Data1colonne mise à jour pour certaines lignes et la Data3colonne 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. Étant donné que certaines lignes ont été Data1mises à jour, ce qui fait partie de la COLUMNS_UPDATED()comparaison, toutes les lignes vues par le déclencheur sont insérées dans le TriggerHistorytableau. 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;

Le TriggerResulttableau a maintenant des lignes potentiellement trompeuses qui semblent n'appartenir à aucun, car elles ne montrent absolument aucun changement (aux deux colonnes de ce tableau). Dans le 2e 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 Data3colonne mise à jour; cependant, comme une ligne du lot a été Data1mise à jour, toutes les lignes sont insérées dans le TriggerResulttableau.

entrez la description de l'image ici

Alternativement, comme @AaronBertrand et @srutzky l'ont souligné, vous pouvez effectuer une comparaison des données réelles dans les tables virtuelles insertedet deleted. Étant donné que la structure des deux tables est identique, vous pouvez utiliser une EXCEPTclause 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 /programming/297960/hash-collision-what-are-the-chances pour une discussion sur les chances infiniment faibles que le calcul HASHBYTES puisse également entraîner des collisions. Preshing a également une analyse décente de ce problème.

Max Vernon
la source
2
C'est une bonne info, mais "Si vous ne pouvez pas gérer cela, vous pouvez utiliser à la HASHBYTESplace." est trompeur. Il est vrai qu'il HASHBYTESest moins susceptible d'avoir de faux négatifs que CHECKSUM(probabilité variant selon la taille de l'algorithme utilisé), mais cela ne peut être exclu. Toute fonction de hachage aura toujours le potentiel d'avoir des collisions car il est fort probable qu'il s'agisse d'informations réduites. La seule façon d'être certain d' aucun changement est de comparer les INSERTEDet DELETEDtables, et en utilisant une _BIN2collation , si les données de chaîne. La comparaison des hachages ne donne qu'une certitude quant aux différences.
Solomon Rutzky
2
@srutzky Si nous voulons nous inquiéter des collisions, précisons également leur probabilité. stackoverflow.com/questions/297960/…
Dave
1
@ Dave Je ne dis pas de ne pas utiliser de hachage: utilisez-les pour identifier les éléments qui ont changé. Mon point est que, puisque la probabilité> 0%, il devrait être indiqué plutôt que sous-entendu qu'il est garanti (le libellé actuel que j'ai cité) afin que les lecteurs le comprennent mieux. Oui, la probabilité d'une collision est très, très faible, mais pas nulle, et varie selon la taille des données source. Si je dois garantir que deux valeurs sont identiques, je passerai quelques cycles CPU supplémentaires à vérifier. Selon la taille du hachage, il peut ne pas y avoir beaucoup de différence de perf entre le hachage et une comparaison BIN2, alors optez pour le 100% précis.
Solomon Rutzky
1
Merci d'avoir ajouté cette note de bas de page (+1). Personnellement, j'utiliserais une ressource autre que cette réponse particulière car elle est trop simpliste. Il y a deux problèmes: 1) à mesure que la taille des valeurs source augmente, la probabilité augmente. J'ai lu plusieurs articles sur SO et d'autres sites hier soir, et une personne qui utilise cela sur des images a signalé des collisions après 25 000 entrées, et 2) la probabilité est juste que, risque relatif, il n'y a rien à dire que quelqu'un utilisant un hachage ne le fera pas rencontrer des collisions à quelques reprises dans 10 000 entrées. Chance = chance. Vous pouvez compter sur vous si vous savez que c'est de la chance ;-).
Solomon Rutzky