Como usar COLUMNS_UPDATED para verificar se alguma coluna está atualizada?

12

Eu tenho tabela com 42 colunas e um gatilho que deve fazer algumas coisas quando 38 dessas colunas são atualizadas. Então, eu preciso pular a lógica se as 4 colunas restantes forem alteradas.

Posso usar a função UPDATE () e criar uma grande IFcondição, mas prefiro fazer algo mais curto. Usando COLUMNS_UPDATED , posso verificar se todas as colunas estão atualizadas?

Por exemplo, verificando se as colunas 3, 5 e 9 são atualizadas:

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

insira a descrição da imagem aqui

Portanto, valor 20para coluna 3e 5, e valor 1para coluna, 9pois é definido no primeiro bit do segundo byte. Se eu alterar a instrução, ORela verificará se as colunas 3e / 5ou a coluna 9estão atualizadas?

Como aplicar a ORlógica no contexto de um byte?

gotqn
fonte
7
Bem, você quer saber se essas colunas são mencionadas na SETlista ou se os valores realmente foram alterados? Ambos UPDATEe COLUMNS_UPDATED()apenas lhe dizem o primeiro. Se você quiser saber se os valores realmente mudaram, será necessário fazer uma comparação adequada de insertede deleted.
Aaron Bertrand
Em vez de usar SUBSTRINGpara dividir o formulário retornado pelo valor COLUMNS_UPDATED(), você deve usar uma comparação bit a bit, conforme mostrado na documentação . Lembre-se de que, se você alterar a tabela de alguma forma, a ordem dos valores retornados por COLUMNS_UPDATED()será alterada.
Max Vernon
Como o @AaronBertrand aludiu, se você precisar ver valores que foram alterados, mesmo que não tenham sido explicitamente atualizados usando uma instrução SETou UPDATE, convém usar CHECKSUM()ou BINARY_CHECKSUM(), ou mesmo HASHBYTES()sobre as colunas em questão.
Max Vernon

Respostas:

17

Você pode usar CHECKSUM()como uma metodologia bastante simples para comparar valores reais para ver se eles foram alterados. CHECKSUM()irá gerar uma soma de verificação em uma lista de valores passados, dos quais o número e o tipo são indeterminados. Cuidado, há uma pequena chance de comparar somas de verificação como essa, resultando em falsos negativos. Se você não consegue lidar com isso, você pode usar HASHBYTESem vez de 1 .

O exemplo abaixo usa um AFTER UPDATEgatilho para manter um histórico de modificações feitas na TriggerTesttabela somente se um dos valores nas colunas Data1 ou for Data2 alterado. Se for Data3alterado, nenhuma ação será tomada.

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

insira a descrição da imagem aqui

Se você insistir em usar a função COLUMNS_UPDATED () , não deverá codificar o valor ordinal das colunas em questão, pois a definição da tabela pode mudar, o que pode invalidar os valores codificados. Você pode calcular qual deve ser o valor em tempo de execução usando as tabelas do sistema. Esteja ciente de que a COLUMNS_UPDATED()função retornará true para o bit especificado da coluna se a coluna for modificada em QUALQUER linha afetada pela UPDATE TABLEinstrução.

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;

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

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

insira a descrição da imagem aqui

Esta demonstração insere linhas na tabela de histórico que talvez não devam ser inseridas. As linhas tiveram sua Data1coluna atualizada para algumas linhas e a Data3coluna atualizada para algumas linhas. Como essa é uma instrução única, todas as linhas são processadas por uma única passagem pelo gatilho. Como algumas linhas foram Data1atualizadas, o que faz parte da COLUMNS_UPDATED()comparação, todas as linhas vistas pelo acionador são inseridas na TriggerHistorytabela. Se isso estiver "incorreto" para o seu cenário, talvez você precise manipular cada linha separadamente, usando um 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;

A TriggerResulttabela agora tem algumas linhas potencialmente enganosas que parecem não pertencer, pois não mostram absolutamente nenhuma alteração (nas duas colunas dessa tabela). No segundo conjunto de linhas da imagem abaixo, o TriggerTestID 7 é o único que parece ter sido modificado. As outras linhas apenas tiveram a Data3coluna atualizada; no entanto desde a uma linha no lote tinha Data1atualizado, todas as linhas são inseridas na TriggerResulttabela.

insira a descrição da imagem aqui

Como alternativa, como apontaram @AaronBertrand e @srutzky, é possível realizar uma comparação dos dados reais nas tabelas insertede deletedvirtual. Como a estrutura de ambas as tabelas é idêntica, você pode usar uma EXCEPTcláusula no gatilho para capturar linhas nas quais as colunas precisas em que você está interessado foram alteradas:

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 uma discussão da pequena chance de que o cálculo do HASHBYTES também possa resultar em colisões. Preshing também tem uma análise decente desse problema.

Max Vernon
fonte
2
Esta é uma boa informação, mas "Se você não pode lidar com isso, pode usar HASHBYTES". é enganoso. É verdade que HASHBYTESé menos provável que haja um falso negativo do que CHECKSUM(probabilidade que varia no tamanho do algoritmo usado), mas não pode ser descartado. Qualquer função de hash sempre terá o potencial de ter colisões, pois é provável que haja informações reduzidas. A única maneira de ter certeza de nenhuma mudança é comparar os INSERTEDe DELETEDtabelas, e usando um _BIN2agrupamento se for dados de cadeia. Comparar hashes apenas garante segurança para as diferenças.
Solomon Rutzky 17/03/16
2
@srutzky Se vamos nos preocupar com colisões, também declaremos a probabilidade delas. stackoverflow.com/questions/297960/…
Dave
1
@ Dave não estou dizendo para não usar hashes: use-os para identificar itens que foram alterados. Meu argumento é que, como a probabilidade> 0%, deve-se afirmar, em vez de implicar, que é garantida (a redação atual que citei) para que os leitores a entendam melhor. Sim, a probabilidade de uma colisão é muito, muito pequena, mas não zero, e varia de acordo com o tamanho dos dados de origem. Se eu precisar garantir que dois valores sejam iguais, gastarei alguns ciclos extras de CPU para verificar. Dependendo do tamanho do hash, pode não haver muita diferença de desempenho entre o hash e a comparação do BIN2, portanto escolha o 100% exato.
Solomon Rutzky 18/03/16
1
Obrigado por inserir essa nota de rodapé (+1). Pessoalmente, eu usaria um recurso diferente daquela resposta específica, pois é excessivamente simplista. Há dois problemas: 1) à medida que os tamanhos dos valores-fonte aumentam, a probabilidade aumenta. Eu li várias postagens no SO e em outros sites ontem à noite, e uma pessoa que usou isso em imagens relatou colisões após 25.000 entradas e 2) probabilidade é apenas que, risco relativo, não há nada a dizer que alguém usando um hash não colidir algumas vezes em 10 mil entradas. Chance = sorte. Não há problema em confiar se você está ciente de que é sorte ;-).
Solomon Rutzky 18/03/16