COLUMNS_UPDATED를 사용하여 특정 열이 업데이트되었는지 확인하는 방법은 무엇입니까?


13

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

여기에 이미지 설명을 입력하십시오

따라서 20column 3및의 51과 column의 값 9은 두 번째 바이트의 첫 번째 비트에 설정되므로 문을 변경 OR하면 열 3및 / 5또는 열 9이 업데이트 되었는지 확인 합니까?

OR1 바이트 컨텍스트에서 로직을 어떻게 적용 할 수 있습니까?


7
글쎄, 그 열이 SET목록에 언급되어 있는지 또는 실제로 값이 변경되었는지 알고 싶 습니까? 모두 UPDATECOLUMNS_UPDATED()단지 당신에게 전자를 말한다. 당신이 값이 실제로 변경 알고 싶은 경우에, 당신은의 적절한 비교를해야 inserted하고 deleted.
Aaron Bertrand

SUBSTRING반환 된 값을 양식으로 나누는 데 사용 하는 대신 설명서에COLUMNS_UPDATED() 표시된대로 비트 비교를 사용해야합니다 . 어떤 식 으로든 테이블을 변경하면 반환되는 값의 순서 가 변경됩니다. COLUMNS_UPDATED()
Max Vernon

@AaronBertrand 당신이 명시 적으로 업데이트되지 않은 경우에도 사용하여 변경된 값을 참조해야하는 경우에 aluded으로 SET또는 UPDATE문을 사용보고 할 수 있습니다 CHECKSUM()하거나 BINARY_CHECKSUM(), 또는 HASHBYTES()문제의 열을 통해.
Max Vernon

답변:


18

CHECKSUM()실제 값을 비교하여 값이 변경되었는지 확인하는 매우 간단한 방법으로 사용할 수 있습니다 . CHECKSUM()전달 된 값 목록에 걸쳐 체크섬을 생성하며 그 중 숫자와 유형이 결정되지 않습니다. 이와 같은 체크섬을 비교할 가능성이 적다는 점에 유의하십시오. 이를 처리 할 수 ​​없으면 1을HASHBYTES 대신 사용할 수 있습니다 .

아래 예는 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;

여기에 이미지 설명을 입력하십시오

이 데모는 삽입해서는 안되는 행을 기록 테이블에 삽입합니다. 행 Data1에 일부 행에 대한 Data3열이 업데이트되고 일부 행에 대해 열이 업데이트되었습니다. 이것은 단일 명령문이므로 모든 행은 트리거를 통한 단일 패스로 처리됩니다. 비교의 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표는 지금은 (해당 테이블의 두 열을) 전혀 변화를 보여주지 이후 속해 그렇지 않은과 같이 몇 가지 잠재적으로 오해의 소지가 행이 있습니다. 아래 이미지의 두 번째 행 집합에서 TriggerTestID 7은 수정 된 것처럼 보이는 유일한 행입니다. 다른 행에는 Data3열만 업데이트되었습니다. 그러나 배치 의 행이 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- HASHBYTES 계산으로 인해 충돌이 발생할 수있는 작은 기회에 대한 설명 은 /programming/297960/hash-collision-what-are-the-chances 를 참조 하십시오 . Preshing 은이 문제에 대한 적절한 분석을 가지고 있습니다.


2
이것은 좋은 정보이지만 "만약 그것을 처리 할 수 ​​없다면 HASHBYTES대신 사용할 수 있습니다 ." 오해의 소지가 있습니다. 그것은 그 사실 HASHBYTES이다 보다가 음성 (false negative) 한 것으로 CHECKSUM(사용 된 알고리즘의 크기에 대한 가능성 VARYING을),하지만 배제 할 수 없다. 해시 함수는 정보가 줄어들 가능성이 높기 때문에 항상 충돌 가능성이 있습니다. 변경 사항없는 유일한 방법 은 INSERTEDDELETED테이블 을 비교하고 _BIN2문자열 데이터 인 경우 데이터 정렬을 사용하는 것입니다. 해시를 비교하면 차이점이 확실합니다.
Solomon Rutzky

2
@srutzky 충돌에 대해 걱정한다면 그 가능성에 대해서도 이야기 해 보자. stackoverflow.com/questions/297960/…
Dave

1
@Dave 나는 해시를 사용하지 말라고 말하는 것이 아닙니다. 변경된 항목을 식별하기 위해 해시를 사용하십시오. 필자의 요점은 가능성이 0 %보다 높기 때문에 그것이 보증된다는 것을 암시하는 것이 아니라 진술해야한다. 예, 충돌 확률은 매우 작지만 0은 아니며 소스 데이터의 크기에 따라 다릅니다. 두 값이 동일하다는 것을 보장해야하는 경우 확인을 위해 몇 번의 추가 CPU주기를 사용합니다. 해시 크기에 따라 해시와 BIN2 비교간에 성능 차이가 크지 않을 수 있으므로 100 % 정확한 값으로 이동하십시오.
Solomon Rutzky

1
각주 (+1)를 넣어 주셔서 감사합니다. 개인적으로, 나는 그 답이 지나치게 단순하기 때문에 특정 답변 이외의 리소스를 사용할 것입니다. 두 가지 문제가 있습니다. 1) 소스 값 크기가 커질수록 확률이 높아집니다. 나는 지난 밤 SO와 다른 사이트에서 여러 게시물을 읽었으며, 한 사람이 이미지에 25,000 항목을 입력 한 후에 충돌을보고했으며 2) 확률은 상대적 위험입니다. 해시를 사용하는 사람은 그렇지 않을 것이라고 말할 수는 없습니다 10k 항목에서 몇 번 충돌이 발생합니다. 기회 = 행운. 운이 좋다는 것을 알고 있다면 의존하는 것이 좋습니다. ;-).
Solomon Rutzky 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.