COLUMNS_UPDATEDを使用して、特定の列のいずれかが更新されているかどうかを確認する方法


12

42列のテーブルと、これらの列のうち38列が更新されたときに何らかの処理を行うトリガーがあります。したがって、残りの4列が変更された場合、ロジックをスキップする必要があります。

私が使用することができUPDATE()関数と一つの大きな作成IF条件を、より短い何かをすることを好みます。COLUMNS_UPDATEDを使用すると、特定の列がすべて更新されているかどうかを確認できますか?

たとえば、列3、5、および9が更新されているかどうかを確認します。

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

ここに画像の説明を入力してください

したがって、203との5値、および1列の値9は2番目のバイトの最初のビットに設定されているためです。ステートメントを変更するORと、列3および/ 5または列9が更新されているかどうかが確認されますか?

OR1バイトのコンテキストでロジックを適用するにはどうすればよいですか?


7
さて、これらの列がSETリストに記載されているかどうか、または値が実際に変更されたかどうかを知りたいですか?両方UPDATECOLUMNS_UPDATED()だけあなたの元を教えてください。値が実際に変更されたかどうかを知りたい場合は、との適切な比較を行う必要がinsertedありdeletedます。
アーロンバートランド

SUBSTRING返される値をformに分割する代わりCOLUMNS_UPDATED()に、ドキュメントに示されているように、ビットごとの比較を使用する必要があります。テーブルを何らかの方法でCOLUMNS_UPDATED()変更すると、返される値の順序が変わることに注意してください。
マックスヴァーノン

@AaronBertrandがにaludedとして、あなたは、彼らが明示的に使用して更新されていなかったにもかかわらず、変更された値を参照する必要がある場合、SETまたはUPDATE文を、あなたが使用して見てみたいことCHECKSUM()BINARY_CHECKSUM()、あるいはHASHBYTES()問題の列の上に。
マックスヴァーノン

回答:


17

CHECKSUM()実際の値を比較して、変更されたかどうかを確認するためのかなり単純な方法論として使用できます。 CHECKSUM()渡された値のリストにわたってチェックサムを生成します。その値とタイプは不定です。注意してください、このようにチェックサムを比較すると、偽陰性になる可能性があります。それに対処できない場合は、HASHBYTES代わりに1を使用できます。

次の例では、AFTER UPDATEトリガーを使用TriggerTestして、Data1 または Data2列のいずれかの値が変更された場合にのみ、テーブルに加えられた変更の履歴を保持します。Data3変更された場合、アクションは実行されません。

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

ここに画像の説明を入力してください

COLUMNS_UPDATED()関数の使用に固執している場合、テーブル定義が変更され、ハードコードされた値が無効になる可能性があるため、問題の列の順序値をハードコードしないでください。システムテーブルを使用して、実行時の値を計算できます。あることに注意してくださいCOLUMNS_UPDATED()列がに変更された場合、関数は指定された列のビットのためにtrueを返すANYの影響を受けた行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;

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

このデモでは、おそらく挿入すべきではない行を履歴テーブルに挿入します。行の一部の行についてData1Data3列が更新されており、一部の行については列が更新されています。これは単一のステートメントであるため、すべての行はトリガーを1回通過することで処理されます。比較のData1一部である一部の行が更新されているためCOLUMNS_UPDATED()、トリガーから見たすべての行がTriggerHistoryテーブルに挿入されます。これがシナリオにとって「正しくない」場合、カーソルを使用して各行を個別に処理する必要があります。

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;

このTriggerResultテーブルには、(そのテーブルの2つの列に)まったく変更がないため、属していないと思われる誤解を招く可能性のある行がいくつかあります。下の画像の2番目の行セットでは、TriggerTestID 7が変更されたように見えます。他の行では列のみがData3更新されました。ただし、バッチの1つの行がData1更新されているため、すべての行がTriggerResultテーブルに挿入されます。

ここに画像の説明を入力してください

または、@ AaronBertrandと@srutzkyが指摘したように、仮想テーブルinserteddeleted仮想テーブルの実際のデータの比較を実行できます。両方のテーブルの構造は同一EXCEPTであるため、トリガーで句を使用して、関心のある正確な列が変更された行をキャプチャできます。

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- https: //stackoverflow.com/questions/297960/hash-collision-what-are-the-chancesを参照してください。HASHBYTES計算によって衝突が発生する可能性が非常に低い可能性についての議論があります。 Preshingはこの問題についてもきちんと分析しています。


2
これは良い情報ですが、「対処できない場合は、HASHBYTES代わりに使用できます。」誤解を招く。(使用されるアルゴリズムのサイズによって可能性が異なる)よりも偽陰性の可能性HASHBYTES低いのは事実ですがCHECKSUM、それを排除することはできません。ハッシュ関数は情報が少なくなる可能性が高いため、常に衝突する可能性があります。変更ないことを確認する唯一の方法はINSERTEDDELETEDテーブルとテーブルを比較し、_BIN2文字列データの場合は照合を使用することです。ハッシュを比較しても、違いは確実になります。
ソロモンラッツキー

2
@srutzky衝突が心配になる場合は、その可能性も述べましょう。stackoverflow.com/questions/297960/…–
デイブ

1
@Daveハッシュを使用しないと言っているわけではありません。ハッシュを使用して、変更されたアイテムを識別します。私のポイントは、可能性が0%を超えているので、読者がそれをよりよく理解できるように、それが保証されていることを暗示するのではなく述べるべきです(私が引用した現在の文言)。はい、衝突の可能性は非常に小さいですが、ゼロではなく、ソースデータのサイズによって異なります。2つの値が同じであることを保証する必要がある場合は、チェックのためにいくつかの余分なCPUサイクルを費やします。ハッシュサイズによっては、ハッシュとBIN2の比較のパフォーマンスに大きな違いはない可能性があるため、100%正確な比較に進みます。
ソロモンラッツキー

1
その脚注を入れてくれてありがとう(+1)。個人的には、過度に単純化されているため、特定の回答以外のリソースを使用します。2つの問題があります。1)ソース値のサイズが大きくなると、確率が高くなります。昨夜、SOや他のサイトでいくつかの投稿を読みましたが、画像でこれを使用している1人が25,000エントリ後に衝突を報告しました。 1万エントリで数回衝突が発生します。チャンス=運。それが運であることを知っているなら、それを信頼しても大丈夫です;-)。
ソロモンラッツキー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.