Bagaimana cara menggunakan COLUMNS_UPDATED untuk memeriksa apakah ada kolom tertentu yang diperbarui?

12

Saya memiliki tabel dengan 42 kolom dan pemicu yang harus melakukan beberapa hal ketika 38 kolom ini diperbarui. Jadi, saya perlu melewati logika jika 4 kolom sisanya diubah.

Saya dapat menggunakan fungsi UPDATE () dan membuat satu IFkondisi besar , tetapi lebih suka melakukan sesuatu yang lebih pendek. Menggunakan COLUMNS_UPDATED saya dapat memeriksa apakah semua kolom tertentu diperbarui?

Misalnya, memeriksa apakah kolom 3, 5 dan 9 diperbarui:

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

masukkan deskripsi gambar di sini

Jadi, nilai 20untuk kolom 3dan 5, dan nilai 1untuk kolom 9karena diatur dalam bit pertama dari byte kedua. Jika saya mengubah pernyataan untuk ORitu akan memeriksa apakah kolom 3dan 5atau kolom 9diperbarui?

Bagaimana bisa menerapkan ORlogika dalam konteks satu byte?

Gotqn
sumber
7
Nah, apakah Anda ingin tahu apakah kolom-kolom itu disebutkan dalam SETdaftar, atau apakah nilainya benar-benar berubah? Keduanya UPDATEdan COLUMNS_UPDATED()hanya memberitahu Anda yang pertama. Jika Anda ingin tahu apakah nilainya benar-benar berubah, Anda harus melakukan perbandingan inserteddan deleted.
Aaron Bertrand
Alih-alih menggunakan SUBSTRINGuntuk membagi formulir nilai yang dikembalikan COLUMNS_UPDATED(), Anda harus menggunakan perbandingan bitwise, seperti yang ditunjukkan dalam dokumentasi . Berhati-hatilah bahwa jika Anda mengubah tabel dengan cara apa pun, urutan nilai yang dikembalikan oleh COLUMNS_UPDATED()akan berubah.
Max Vernon
Seperti yang disinggung oleh @AaronBertrand, jika Anda perlu melihat nilai yang diubah meskipun tidak diperbarui secara eksplisit menggunakan pernyataan SETatau UPDATE, Anda mungkin ingin melihat menggunakan CHECKSUM()atau BINARY_CHECKSUM(), atau bahkan di HASHBYTES()atas kolom yang dimaksud.
Max Vernon

Jawaban:

17

Anda dapat menggunakan CHECKSUM()metodologi yang cukup sederhana untuk membandingkan nilai aktual untuk melihat apakah mereka diubah. CHECKSUM()akan menghasilkan checksum di seluruh daftar nilai yang diteruskan, di mana jumlah dan jenisnya tidak ditentukan. Hati-hati, ada kemungkinan kecil membandingkan checksum seperti ini akan menghasilkan negatif palsu. Jika Anda tidak dapat menghadapi itu, Anda dapat menggunakan HASHBYTESsebagai gantinya 1 .

Contoh di bawah ini menggunakan AFTER UPDATEpemicu untuk mempertahankan riwayat modifikasi yang dibuat ke TriggerTesttabel hanya jika salah satu nilai dalam kolom Data1 atau Data2 berubah. Jika Data3berubah, tidak ada tindakan yang diambil.

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

masukkan deskripsi gambar di sini

Jika Anda ngotot menggunakan fungsi COLUMNS_UPDATED () , Anda tidak boleh meng -hard-code nilai ordinal kolom yang bersangkutan, karena definisi tabel mungkin berubah, yang bisa membuat nilai-nilai hard-code tidak valid. Anda bisa menghitung berapa nilainya saat runtime menggunakan tabel sistem. Ketahuilah bahwa COLUMNS_UPDATED()fungsi mengembalikan true untuk bit kolom yang diberikan jika kolom diubah dalam baris APA PUN yang dipengaruhi oleh UPDATE TABLEpernyataan.

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;

masukkan deskripsi gambar di sini

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

masukkan deskripsi gambar di sini

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

masukkan deskripsi gambar di sini

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

masukkan deskripsi gambar di sini

Demo ini menyisipkan baris ke tabel sejarah yang mungkin tidak boleh dimasukkan. Baris telah Data1diperbarui kolomnya untuk beberapa baris, dan Data3kolom diperbarui untuk beberapa baris. Karena ini adalah pernyataan tunggal, semua baris diproses oleh satu melewati pemicu. Karena beberapa baris telah Data1diperbarui, yang merupakan bagian dari COLUMNS_UPDATED()perbandingan, semua baris yang dilihat oleh pelatuk dimasukkan ke dalam TriggerHistorytabel. Jika ini "salah" untuk skenario Anda, Anda mungkin perlu menangani setiap baris secara terpisah, menggunakan kursor.

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;

The TriggerResultmeja sekarang memiliki beberapa baris berpotensi menyesatkan yang terlihat seperti mereka tidak milik karena mereka menunjukkan benar-benar tidak ada perubahan (ke dua kolom dalam tabel itu). Dalam rangkaian baris ke-2 pada gambar di bawah ini, TriggerTestID 7 adalah satu-satunya yang sepertinya dimodifikasi. Baris lain hanya memiliki Data3kolom diperbarui; namun karena satu baris dalam kumpulan telah Data1diperbarui, semua baris dimasukkan dalam TriggerResulttabel.

masukkan deskripsi gambar di sini

Sebagai alternatif, seperti yang ditunjukkan oleh @AaronBertrand dan @srutzky, Anda dapat melakukan perbandingan data aktual di inserteddan deletedtabel virtual. Karena struktur kedua tabel identik, Anda bisa menggunakan EXCEPTklausa di pemicu untuk menangkap baris di mana kolom yang tepat Anda tertarik telah berubah:

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 - lihat /programming/297960/hash-collision-what-are-the-chances untuk diskusi tentang peluang kecil yang semakin menghilang sehingga perhitungan HASHBYTES juga dapat mengakibatkan tabrakan. Preshing juga memiliki analisis yang layak untuk masalah ini.

Max Vernon
sumber
2
Ini info yang bagus, tetapi "Jika Anda tidak bisa mengatasinya, Anda bisa menggunakannya HASHBYTES." menyesatkan. Memang benar bahwa HASHBYTESadalah kurang mungkin untuk memiliki negatif palsu dari CHECKSUM(kemungkinan yang bervariasi pada ukuran algoritma yang digunakan), tetapi tidak dapat dikesampingkan. Setiap fungsi hashing akan selalu memiliki potensi untuk mengalami tabrakan karena kemungkinan besar akan berkurang informasinya. Satu-satunya cara untuk menjadi tertentu tidak ada perubahan adalah untuk membandingkan INSERTEDdan DELETEDtabel, dan menggunakan _BIN2pemeriksaan jika data string. Membandingkan hash hanya memberikan kepastian untuk perbedaan.
Solomon Rutzky
2
@srutzky Jika kita akan khawatir tentang tabrakan, mari kita juga menyatakan kemungkinannya. stackoverflow.com/questions/297960/…
Dave
1
@ Saya tidak mengatakan jangan menggunakan hash: gunakan itu untuk mengidentifikasi item yang telah berubah. Maksud saya adalah, karena kemungkinan> 0%, harus dinyatakan daripada tersirat bahwa itu dijamin (kata-kata saat ini yang saya kutip) sehingga pembaca memahaminya dengan lebih baik. Ya, probabilitas tabrakan sangat, sangat kecil, tetapi tidak nol, dan bervariasi berdasarkan ukuran data sumber. Jika saya perlu menjamin bahwa dua nilainya sama, saya akan menghabiskan beberapa siklus CPU tambahan untuk memeriksa. Bergantung pada ukuran hash, mungkin tidak ada banyak perbedaan antara hash dan BIN2, jadi gunakan 100% akurat.
Solomon Rutzky
1
Terima kasih telah memasukkan catatan kaki itu (+1). Secara pribadi, saya akan menggunakan sumber daya selain dari jawaban tertentu karena terlalu sederhana. Ada dua masalah: 1) ketika ukuran nilai sumber semakin besar, probabilitas meningkat. Saya membaca beberapa posting di SO dan situs lain tadi malam, dan satu orang menggunakan ini pada gambar melaporkan tabrakan setelah 25.000 entri, dan 2) kemungkinan hanya itu, risiko relatif, tidak ada yang mengatakan bahwa seseorang yang menggunakan hash tidak akan mengalami tabrakan beberapa kali dalam entri 10k. Peluang = keberuntungan. Tidak apa-apa untuk mengandalkan jika Anda sadar itu keberuntungan ;-).
Solomon Rutzky