必须使用AFTER触发器,因其能确保事务提交、主键生成和数据落地后再写日志,避免回滚导致审计失效;AFTER中OLD/NEW代表最终值,配合JSON_OBJECT()结构化记录变化字段,审计表需规范设计字段类型与索引,且触发器内禁止查表、调过程或改业务表。

必须用 AFTER 触发器,不能用 BEFORE —— 否则日志和实际数据不一致是大概率事件。
为什么只用 AFTER INSERT/UPDATE/DELETE
BEFORE 触发器里 OLD 和 NEW 虽然可读,但事务还没提交:自增主键可能仍是 NULL(AFTER INSERT 才确定),外键约束也可能未生效,更关键的是——事务若回滚,日志已写入,数据却没变,审计就失效了。
只有 AFTER 能确保变更已落库、主键已生成、事务上下文完整。
-
AFTER INSERT:只读NEW.*,此时主键值已稳定 -
AFTER UPDATE:OLD.*和NEW.*都可用,且都代表最终落地值 -
AFTER DELETE:OLD.*仍可读,即使触发级联删除,也能拿到被删行快照
JSON_OBJECT() 是构造日志值的唯一推荐方式
别拼字符串:CONCAT 遇到单引号、换行、NULL 就崩,且无法被下游解析为结构化数据。JSON_OBJECT() 自动处理 NULL → JSON null,字段名必须用字符串字面量(如 'name'),不能写变量名。
- 只记录变化字段:比如只关心
status变更,就只写JSON_OBJECT('status', OLD.status, 'updated_at', NOW()) - 全量记录也行,但字段要显式列出:
JSON_OBJECT('id', NEW.id, 'name', NEW.name) - 字段类型必须匹配原表,否则插入日志表时会截断或报错
Data too long
审计表结构不能偷懒,字段设计决定后期能不能查
光存整行 JSON 看似省事,但没法索引、无法按字段统计、查起来慢得像翻纸书。真正能用的日志表至少含:
-
table_name(VARCHAR(64)):方便跨表统一查 -
row_id(类型严格匹配原表主键,比如BIGINT):别用VARCHAR存数字 ID,否则关联查询性能崩 -
operation(ENUM('INSERT','UPDATE','DELETE')):比字符串节省空间,避免索引失效 -
old_value/new_value(JSON类型):字段级差异,不是整行 dump -
created_at(用NOW(),别用SYSDATE()):后者在事务中可能因延迟产生歧义 -
user_id(应用层传入,别用CURRENT_USER()或USER()):它们返回数据库账号,不是业务用户
触发器里禁止做三件事:查表、调过程、改其他业务表
触发器运行在主 DML 的临界路径上,任何额外开销都会直接拖慢业务 SQL。曾有案例在 AFTER UPDATE 里 SELECT ... JOIN user_profiles 补用户名,结果主表更新卡住,连接池耗尽。
- 禁止
SELECT INTO、子查询、关联大表 - 禁止调用含事务控制的存储过程(会报
Can't execute statement in trigger) - 禁止在触发器里再
INSERT/UPDATE其他业务表——容易引发循环触发或死锁 - 日志表引擎必须用
InnoDB,保证与主表事务一致性
最易被忽略的点:一个表最多只能有一个 AFTER INSERT、一个 AFTER UPDATE、一个 AFTER DELETE 触发器;重复定义会直接报错 Can't create more than one trigger with the same action time and event for one table。线上高频表启用后,务必监控日志表写入 QPS 和磁盘增长,膨胀过快就得加归档策略。


















