必须手动比对字段值并显式处理NULL,不能依赖IF UPDATE()或通配符;SQL Server需用inserted/deleted表JOIN比对,MySQL需逐字段用OLD/NEW判断,PostgreSQL需RETURN NEW/OLD且用row_to_json。

不能靠 IF UPDATE() 或 OLD.*/NEW.* 通配直接拿到“哪些字段变了”,必须手动比对值、显式处理 NULL、分操作类型写逻辑——否则日志里全是假变更或漏记录。
SQL Server 里怎么判断某字段值真变了
IF UPDATE(Email) 只表示 SQL 语句里写了 SET Email = ...,哪怕前后值一样也返回 TRUE。真实差异检测只能靠 inserted 和 deleted 表比对。
- 必须用
INNER JOIN关联两表,主键字段一个都不能少 - NULL 比较不能用
=或!=,得写成:i.Email != d.Email OR (i.Email IS NULL AND d.Email IS NOT NULL) OR (i.Email IS NOT NULL AND d.Email IS NULL) - SQL Server 2022+ 可用更简洁的
i.Email IS DISTINCT FROM d.Email - 别在
WHERE里直接比大字段(如NVARCHAR(MAX)),先加LEN()判断长度是否一致再进深层比对
MySQL 触发器中逐字段记录变更的写法
MySQL 不支持 IF UPDATE(),也没 inserted/deleted 表,只能靠 OLD.col 和 NEW.col 手动判断。UPDATE 触发器里必须显式写出每个要监控的字段比较逻辑。
- INSERT 场景只用
NEW.col,DELETE 只用OLD.col,UPDATE 才能两者都用 - 字段多时别堆
IF OLD.name != NEW.name THEN ... END IF;套嵌,改用存储过程封装比对逻辑,或在应用层生成触发器脚本 - JSON 类型字段可用
JSON_CONTAINS_PATH(NEW.json_col, 'one', '$.status')配合JSON_EXTRACT提取后比对,但注意 MySQL 5.7 对 JSON 比对不支持IS DISTINCT FROM - 时间戳类字段(如
updated_at)建议在比对前从NEW.*中排除,避免因自动更新导致误判
审计日志表字段设计的关键约束
字段级日志不是把整行塞进去就完事,结构错一点,后续查“谁什么时候把 status 从 A 改成 B”就极难过滤。
- 必须有
table_name、record_id(原表主键值)、field_name(变动字段名)、old_value、new_value五个核心字段 -
old_value和new_value推荐统一用TEXT或NVARCHAR(MAX),别按源字段类型一一对应——否则INT改成VARCHAR会截断 - 不要存二进制或 LOB 值本身,可存哈希(如
SHA2(NEW.blob_col, 256))加标记,避免日志表膨胀过快 - 联合索引至少建在
(table_name, record_id, field_name, created_at)上,不然按业务单据查变更历史会全表扫
跨数据库兼容的字段变更捕获陷阱
PostgreSQL 的 TG_OP、SQL Server 的 inserted/deleted、MySQL 的 OLD/NEW 看似功能相似,但行为差异极大,混用必出问题。
- PostgreSQL 的 AFTER 触发器函数末尾必须
RETURN NEW或RETURN OLD,漏写整个事务回滚;SQL Server 和 MySQL 没这要求 - MySQL 触发器里禁止
SELECT * FROM t_user(哪怕是审计表),会报ERROR 1442;SQL Server 允许查其他表,但不能查自己正在被修改的表 - Oracle 用
:old.col/:new.col,且AFTER触发器无法读到未提交的:new值(受事务隔离影响),必须用BEFORE - 所有数据库里,触发器内获取真实操作人(非数据库连接账号)都不可靠——
USER()、SYS_CONTEXT('USERENV','SESSION_USER')、ORIGINAL_LOGIN()全是连接池账号,必须由应用层通过CONTEXT_INFO、临时表或预留字段传入
字段级变更日志最易被忽略的点:它和业务主流程强耦合,一次 UPDATE 触发 3 行变更,就得写 3 条日志;如果审计表没索引、磁盘慢、或字段值含大 JSON,主 SQL 就卡住——这不是日志功能问题,是同步阻塞的本质决定的。


















