直接写OLD.col != NEW.col会漏记录,因为NULL参与比较结果为UNKNOWN,而IF或WHERE只认TRUE;正确做法是用NULL安全比较操作符,它将两个NULL视为相等,语义清晰且无需额外判断。

为什么直接写 OLD.col != NEW.col 会漏记录
MySQL 中 NULL != 'abc' 或 'abc' != NULL 的结果不是 TRUE 或 FALSE,而是 UNKNOWN,而 IF 或 WHERE 条件里 UNKNOWN 等价于不成立。只要 OLD 或 NEW 任一端为 NULL,!= 就失效——字段明明变了,日志却没记。
正确做法是用 MySQL 的 NULL 安全比较操作符 :它把两个 NULL 视为相等,NULL 'x' 返回 FALSE(因为确实不等),语义清晰且无需额外判断。
-
OLD.email NEW.email→ 相等返回 1,不等返回 0 - 反向判断“是否变化”就写:
NOT (OLD.email NEW.email) - 多列时不要堆砌多个
!=,每列都用单独比
如何只记录真正变化的字段,避免日志膨胀
审计日志不是备份,核心价值在于“哪一列、从什么值变成什么值”。全量记录(比如把整行转 JSON 插进日志表)会导致查询慢、索引无效、存储翻倍。
实操建议按字段粒度拆分插入,用 IF + 控制每列是否写入:
- AFTER UPDATE 触发器中,对每个需审计的字段单独判断:
IF NOT (OLD.name NEW.name) THEN INSERT INTO audit_log (...) VALUES ('name', OLD.name, NEW.name, ...); END IF; - 避免在触发器里拼接长字符串或调用
JSON_OBJECT()—— 这类操作在高并发下 CPU 和内存开销明显上升 - 如果字段很多(如 20+ 列),别手写 20 个
IF,考虑用应用层生成触发器 DDL,或用模板工具批量生成
多列同时变更时,怎么保证原子性和顺序
MySQL 的 AFTER UPDATE 触发器天然按行触发、事务内执行,只要不主动写 COMMIT 或 ROLLBACK(触发器里也不允许),整个触发逻辑就跟原 DML 同属一个事务:主表更新失败,日志也不会落库。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
但要注意两件事:
- 所有
INSERT INTO audit_log必须走同一张表、同一引擎(推荐InnoDB),否则跨引擎可能破坏原子性 - 不要在触发器里做耗时操作:比如调用 UDF、查大表、发 HTTP 请求——这些会让事务持锁时间变长,拖慢主业务
- 如果审计表本身也加了触发器(比如想再同步到 Kafka),必须确认不会形成隐式递归(MySQL 不报错但行为不可控)
复合主键或 JSON 字段怎么安全比对
复合主键字段(如 (order_id, line_no))在 UPDATE 触发器里要小心:不能只比其中一列,否则可能误判;必须全部参与 判断,或改用主键组合字符串比对(如 CONCAT(OLD.order_id, ':', OLD.line_no) != CONCAT(NEW.order_id, ':', NEW.line_no))。
JSON 字段不能直接用 ,因为 JSON 类型在 MySQL 中不支持该操作符。正确方式是:
- 用
JSON_CONTAINS()+JSON_EXTRACT()做结构化比对(适合已知路径) - 更通用的是标准化后转字符串:
MD5(JSON_COMPACT(OLD.config)) != MD5(JSON_COMPACT(NEW.config)) - 时间字段注意精度:
OLD.updated_at != NEW.updated_at可能因毫秒差异总为真,应先截断:DATE_SUB(OLD.updated_at, INTERVAL MICROSECOND_SECOND MICROSECOND)
最易被忽略的一点:触发器里没有“默认忽略 NULL”的捷径,每一列都得显式处理 NULL 安全比对;哪怕只是加个 IS NULL 判断,也比依赖 != 更可靠。

















