T-SQL 触发器 - 审计列更改

T-SQL Trigger - Audit Column Change

给定一个带有 ID 的简单 table,审计正在更改的列的正确方法是什么。我在查看各种似乎不起作用的答案后问。

这是我的:

Create Table Tbl_Audit
(
    AuditId int identity(1,1) not null,
    Tbl_Id int not null.
    Tbl_Old_ColumnValue varchar(255),
    Tbl_New_ColumnValue varchar(255)
)

GO

Create Trigger Tr_Tbl_ColumnChanged on Tbl
after insert, update
As
begin
    if(update(ColumnName))
        begin
            insert into tbl_audit
            (
                Tbl_Id,
                Tbl_Old_ColumnName,
                Tbl_New_ColumnName
            )
            select
                tbl.PKId,
                tbl.ColumnName,
                i.ColumnName,
            from
                Tbl tbl join 
                inserted i
                on tbl.PKId = i.PKId
            
end

我看到的是 Tbl_Old_ColumnValue = Tbl_New_ColumnValue 的数千个示例,这不是我想要的。

我希望 运行:

select top 10 * from tbl_audit where Tbl_Old_ColumnValue !=Tbl_New_ColumnValue 

但是这个returns没有结果。

为了获得实际更改的列的结果,我需要 运行 一个非常昂贵的查询:

select top 10 
old.AuditId,
old.Tbl_Old_ColumnValue,
new.Tbl_Old_ColumnValue  as [Tbl_New_ColumnValue] 
from tbl_audit [old]
join Tbl_Audit [new]
on [ol].Tbl_Id= [new].Tbl_Id and [old].AuditId != [new].AuditId
where [old].Tbl_Old_ColumnValue != [new].Tbl_Old_ColumnValue 

结果:

AuditId Tbl_Id  Tbl_Old_ColumnValue Tbl_New_ColumnValue
10051       1       old_value           old_value
10052       1       new_value           new_value

但这并没有产生我期望的结果:

AuditId Tbl_Id  Tbl_Old_ColumnValue Tbl_New_ColumnValue
10057       1   old_value           Some New Value

奇怪的是,如果我使用以下方法直接通过 SSMS 修改列:

update Tbl set Tbl.ColumnValue = 'Some New Value'

我看到了我对触发器的期望:

AuditId Tbl_Id  Tbl_Old_ColumnValue Tbl_New_ColumnValue
10057       1   old_value           Some New Value

我做错了什么?

此外,如何消除对 update(ColumnName) 实际上为假的行的审核。 IE,ColumnName(即使已设置)在设置为 previous/old 值时未被审计。

update(ColumnName) 并不意味着值已更改,只是该列涉及 insert/update - 并且它将始终涉及插入。您需要使用 inserteddeleted 比较旧值和新值,例如

insert into tbl_audit
(
    Tbl_Id,
    Tbl_Old_ColumnName,
    Tbl_New_ColumnName
)
select
    tbl.PKId,
    tbl.ColumnName,
    i.ColumnName,
from
    inserted i
    left join deleted d on d.PKId = i.PKId
    -- Insert d.PKId is null, there are no records in deleted
    where d.PKId is null
    -- Change from null to value
    or (i.ColumnName is null and d.ColumnName is not null)
    -- Change from value to null
    or (i.ColumnName is not null and d.ColumnName is null)
    -- Change in value
    or i.ColumnName <> d.ColumnName;

您可以使用 coalesce 和一个永远不会实际出现在您的数据中的合适值来简化空值检查。

documentation 实际上在这方面做得很好。

并且如果该列并不总是包含在更新中,那么 update(ColumnName) 测试仍然值得做,因为它加快了触发器的速度,并且触发器应该尽可能快。就我个人而言,我会提前短路,例如if not update(ColumnName) return;

显然您需要调整该逻辑来处理您正在审核的所有列。