วิธีใช้ 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';

ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นค่า20สำหรับคอลัมน์3และ5และค่า1สำหรับคอลัมน์9เนื่องจากถูกตั้งค่าในบิตแรกของไบต์ที่สอง หากฉันเปลี่ยนคำสั่งORเป็นจะตรวจสอบว่าคอลัมน์3และ5หรือคอลัมน์9นั้นมีการ / มีการปรับปรุง?

จะใช้ORตรรกะในบริบทของหนึ่งไบต์ได้อย่างไร


7
ทีนี้คุณอยากรู้ไหมว่าคอลัมน์เหล่านั้นถูกกล่าวถึงในSETรายการหรือไม่หรือถ้าค่านั้นเปลี่ยนไปจริง ๆ ? ทั้งคู่UPDATEและCOLUMNS_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()จะสร้างการตรวจสอบข้ามรายการของค่าที่ส่งผ่านซึ่งจำนวนและประเภทจะไม่แน่นอน ระวังมีโอกาสเล็กน้อยที่จะเปรียบเทียบเช็คซัมแบบนี้จะส่งผลให้เกิดการลบในทางที่ผิด ถ้าคุณไม่สามารถจัดการกับที่คุณสามารถใช้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()ฟังก์ชันจะส่งกลับค่าจริงสำหรับบิตของคอลัมน์ที่กำหนดหากคอลัมน์นั้นได้รับการแก้ไขในแถวใดก็ได้ที่ได้รับผลกระทบจาก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ตารางตอนนี้มีแถวที่อาจทำให้เข้าใจผิดบางอย่างที่ดูเหมือนว่าพวกเขาไม่ได้เป็นเพราะพวกเขาแสดงให้เห็นอย่างไม่มีการเปลี่ยนแปลง (ทั้งสองคอลัมน์ในตารางนั้น) ในแถวที่ 2 ในรูปภาพด้านล่าง TriggerTestID 7 เป็นเพียงแถวเดียวที่ดูเหมือนจะถูกแก้ไข แถวอื่น ๆ มีการData3อัปเดตคอลัมน์เท่านั้น อย่างไรก็ตามตั้งแต่หนึ่งแถวในแบตช์ได้Data1รับการปรับปรุงแถวทั้งหมดจะถูกแทรกในTriggerResultตาราง

ป้อนคำอธิบายรูปภาพที่นี่

อีกวิธีหนึ่งคือ @AaronBertrand และ @srutzky ชี้ให้เห็นคุณสามารถทำการเปรียบเทียบข้อมูลจริงในตารางเสมือนinsertedและ deletedเนื่องจากโครงสร้างของทั้งสองตารางเหมือนกันคุณสามารถใช้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 - ดูที่/programming/297960/hash-collision-what-are-the-chancesสำหรับการแยกแยะโอกาสเล็ก ๆ ที่หายไปซึ่งการคำนวณ HASHBYTES อาจส่งผลให้เกิดการชนกัน Preshingมีการวิเคราะห์ปัญหานี้เช่นกัน


2
นี่เป็นข้อมูลที่ดี แต่ "ถ้าคุณไม่สามารถจัดการกับสิ่งนั้นได้คุณสามารถใช้HASHBYTESแทนได้" กำลังทำให้เข้าใจผิด มันเป็นความจริงที่HASHBYTESมีโอกาสน้อยที่จะมีค่าลบที่ผิดพลาดกว่าCHECKSUM(ความน่าจะเป็นที่แตกต่างกันไปตามขนาดของอัลกอริทึมที่ใช้) แต่ไม่สามารถตัดออกได้ ฟังก์ชั่นการแปลงแป้นพิมพ์ใด ๆ มักจะมีแนวโน้มที่จะมีการชนกันเนื่องจากมีแนวโน้มที่จะลดข้อมูล วิธีเดียวที่จะมั่นใจได้ว่าไม่มีการเปลี่ยนแปลงใด ๆคือการเปรียบเทียบINSERTEDและDELETEDตารางและใช้การ_BIN2เปรียบเทียบหากเป็นข้อมูลสตริง การเปรียบเทียบแฮชจะให้ความแตกต่างเท่านั้น
โซโลมอน Rutzky

2
@srutzky หากเราจะต้องกังวลเกี่ยวกับการชนกันให้ระบุถึงโอกาสของมันด้วย stackoverflow.com/questions/297960/…
เดฟ

1
@Dave ฉันไม่ได้บอกว่าไม่ใช้แฮช: ใช้พวกเขาเพื่อระบุรายการที่มีการเปลี่ยนแปลง ประเด็นของฉันคือเนื่องจากความน่าจะเป็น> 0% มันควรจะระบุไว้มากกว่าที่จะบ่งบอกว่ามันรับประกัน (ถ้อยคำปัจจุบันที่ฉันยกมา) เพื่อให้ผู้อ่านเข้าใจได้ดีขึ้น ใช่ความน่าจะเป็นของการชนนั้นเล็กมาก แต่ไม่เป็นศูนย์และแตกต่างกันไปตามขนาดของแหล่งข้อมูล หากฉันต้องการรับประกันว่าค่าสองค่าเหมือนกันฉันจะใช้ CPU รอบพิเศษเพื่อตรวจสอบ ขึ้นอยู่กับขนาดของแฮชอาจไม่มีความแตกต่างอย่างสมบูรณ์ระหว่างแฮชกับ BIN2 เปรียบเทียบดังนั้นลองใช้ 100% ที่แม่นยำ
โซโลมอน Rutzky

1
ขอขอบคุณที่ใส่เชิงอรรถ (+1) โดยส่วนตัวแล้วฉันจะใช้ทรัพยากรอื่นที่ไม่ใช่คำตอบเฉพาะเพราะมันง่ายเกินไป มีสองประเด็นคือ 1) เมื่อขนาดของแหล่งที่มามีขนาดใหญ่ขึ้นความน่าจะเป็นที่เพิ่มขึ้น ฉันอ่านข้อความหลายโพสต์ใน SO และเว็บไซต์อื่น ๆ เมื่อคืนที่ผ่านมาและมีคนคนหนึ่งที่ใช้สิ่งนี้บนภาพที่รายงานว่ามีการชนกันหลังจาก 25,000 รายการและ 2) ความน่าจะเป็นคือความเสี่ยงสัมพัทธ์ พบการชนกันสองสามครั้งในรายการ 10k โอกาส = โชค มันโอเคถ้าคุณรู้ตัวว่ากำลังโชค ;-)
โซโลมอน Rutzky
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.