¿Cómo usar COLUMNS_UPDATED para verificar si alguna de ciertas columnas está actualizada?

13

Tengo una tabla con 42 columnas y un disparador que debería hacer algunas cosas cuando se actualizan 38 de estas columnas. Entonces, necesito omitir la lógica si se cambian las 4 columnas restantes.

Puedo usar la función UPDATE () y crear una gran IFcondición, pero prefiero hacer algo más corto. Con COLUMNS_UPDATED, ¿puedo verificar si se actualizan todas las columnas?

Por ejemplo, verificar si las columnas 3, 5 y 9 están actualizadas:

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

ingrese la descripción de la imagen aquí

Entonces, valor 20para columna 3y 5, y valor 1para columna 9porque se establece en el primer bit del segundo byte. Si cambio la declaración OR, ¿verificará si las columnas 3y / 5o columna 9se actualizan?

¿Cómo se puede aplicar la ORlógica en el contexto de un byte?

gotqn
fuente
77
Bueno, ¿quieres saber si esas columnas se mencionan en la SETlista o si los valores realmente cambiaron? Ambos UPDATEy COLUMNS_UPDATED()solo te digo lo primero. Si desea saber si los valores realmente cambiaron, deberá hacer una comparación adecuada de insertedy deleted.
Aaron Bertrand
En lugar de usar SUBSTRINGpara dividir el valor del formulario devuelto COLUMNS_UPDATED(), debe usar una comparación bit a bit, como se muestra en la documentación . Tenga en cuenta que si cambia la tabla de alguna manera, cambiará el orden de los valores devueltos por COLUMNS_UPDATED().
Max Vernon
Como mencionó @AaronBertrand, si necesita ver los valores que se modificaron a pesar de que no se actualizaron explícitamente mediante una instrucción SETo UPDATE, es posible que desee ver el uso de CHECKSUM()o BINARY_CHECKSUM(), o incluso HASHBYTES()sobre las columnas en cuestión.
Max Vernon

Respuestas:

18

Puede utilizar CHECKSUM()una metodología bastante simple para comparar valores reales para ver si se modificaron. CHECKSUM()generará una suma de comprobación en una lista de valores pasados, de los cuales el número y el tipo son indeterminados. Cuidado, hay una pequeña posibilidad de comparar sumas de comprobación como esta que dará como resultado falsos negativos. Si no puede hacer frente a eso, se puede utilizar HASHBYTESen lugar de 1 .

El siguiente ejemplo utiliza un AFTER UPDATEactivador para retener un historial de modificaciones realizadas en la TriggerTesttabla solo si alguno de los valores en las columnas Data1 o Data2 cambia. Si Data3cambia, no se toman medidas.

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

ingrese la descripción de la imagen aquí

Si insiste en usar la función COLUMNS_UPDATED () , no debe codificar el valor ordinal de las columnas en cuestión, ya que la definición de la tabla puede cambiar, lo que puede invalidar los valores codificados. Puede calcular cuál debería ser el valor en tiempo de ejecución utilizando las tablas del sistema. Tenga en cuenta que la COLUMNS_UPDATED()función devuelve verdadero para el bit de columna dado si la columna se modifica en CUALQUIER fila afectada por la UPDATE TABLEinstrucción.

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;

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

Esta demostración inserta filas en la tabla de historial que quizás no deberían insertarse. Las filas han tenido su Data1columna actualizada para algunas filas, y la Data3columna ha sido actualizada para algunas filas. Como se trata de una declaración única, todas las filas se procesan con una sola pasada a través del desencadenador. Como algunas filas se han Data1actualizado, lo que forma parte de la COLUMNS_UPDATED()comparación, todas las filas vistas por el desencadenador se insertan en la TriggerHistorytabla. Si esto es "incorrecto" para su escenario, es posible que deba manejar cada fila por separado, utilizando un cursor.

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 TriggerResulttabla ahora tiene algunas filas potencialmente engañosas que parecen no pertenecer, ya que no muestran absolutamente ningún cambio (en las dos columnas de esa tabla). En el segundo conjunto de filas en la imagen a continuación, TriggerTestID 7 es el único que parece haber sido modificado. Las otras filas solo tenían la Data3columna actualizada; sin embargo, desde la una fila en el lote había Data1actualizado, todas las filas se insertan en la TriggerResulttabla.

ingrese la descripción de la imagen aquí

Alternativamente, como @AaronBertrand y @srutzky señalaron, se puede realizar una comparación de los datos reales de los insertedy deletedvirtuales tablas. Dado que la estructura de ambas tablas es idéntica, puede usar una EXCEPTcláusula en el disparador para capturar filas donde las columnas precisas que le interesan han cambiado:

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: consulte /programming/297960/hash-collision-what-are-the-chances para ver una discusión sobre la pequeña posibilidad de que el cálculo HASHBYTES también pueda provocar colisiones. Preshing también tiene un análisis decente de este problema.

Max Vernon
fuente
2
Esta es una buena información, pero "Si no puede lidiar con eso, puede usarla HASHBYTESen su lugar". es engañosa. Es cierto que HASHBYTESes menos probable que tenga falsos negativos que CHECKSUM(la probabilidad varía según el tamaño del algoritmo utilizado), pero no se puede descartar. Cualquier función de hashing siempre tendrá el potencial de tener colisiones, ya que es muy probable que se reduzca la información. La única manera de estar seguro de no hay cambios es comparar las INSERTEDy DELETEDmesas, y usando un _BIN2cotejo si se trata de datos de cadena. Comparar hashes solo da certeza para las diferencias.
Solomon Rutzky
2
@srutzky Si vamos a estar preocupados por las colisiones, también indiquemos la probabilidad de que ocurran. stackoverflow.com/questions/297960/…
Dave
1
@Dave No digo que no uses hashes: úsalos para identificar elementos que han cambiado. Lo que quiero decir es que, dado que la probabilidad es> 0%, debería indicarse en lugar de implicarse que está garantizado (la redacción actual que cité) para que los lectores lo entiendan mejor. Sí, la probabilidad de una colisión es muy, muy pequeña, pero no cero, y varía según el tamaño de los datos de origen. Si necesito garantizar que dos valores son iguales, pasaré algunos ciclos de CPU adicionales para verificar. Dependiendo del tamaño del hash, puede que no haya mucha diferencia de rendimiento entre el hash y una comparación BIN2, así que opta por el 100% exacto.
Solomon Rutzky
1
Gracias por poner esa nota al pie (+1). Personalmente, usaría un recurso que no sea esa respuesta en particular, ya que es demasiado simplista. Hay dos problemas: 1) a medida que aumenta el tamaño del valor fuente, aumenta la probabilidad. Anoche leí varias publicaciones en SO y otros sitios, y una persona que usó esto en imágenes informó colisiones después de 25,000 entradas, y 2) la probabilidad es solo eso, riesgo relativo, no hay nada que decir que alguien que usa un hash no toparse con colisiones varias veces en 10k entradas. Oportunidad = suerte. Está bien confiar si eres consciente de que es suerte ;-).
Solomon Rutzky