MySQL BEFORE UPDATE触发器通过OLD.字段名和NEW.字段名获取更新前后值,INSERT仅可用NEW,DELETE仅可用OLD;必须用BEFORE而非AFTER以确保值稳定且可回滚,避免日志错乱。

触发器里怎么拿到旧值和新值
MySQL 的 BEFORE UPDATE 触发器能通过 OLD.字段名 和 NEW.字段名 分别访问更新前后的值,这是记录变更日志的基础。注意:BEFORE INSERT 只有 NEW,BEFORE DELETE 只有 OLD;想记录“从空到有”或“从有到空”,必须用 BEFORE UPDATE 并显式判断 OLD.字段名 IS NULL 或 NEW.字段名 = ''。
常见错误是直接在 AFTER UPDATE 里读 OLD/NEW——语法允许但值可能已被事务覆盖(尤其高并发时),实际日志内容错乱。务必用 BEFORE。
敏感字段变更怎么高效判断并写入日志表
不要为每个字段写单独的 IF 判断,容易漏、难维护。推荐用 CASE + 字符串拼接,把所有敏感字段的变更统一处理:
INSERT INTO audit_log (table_name, record_id, field_name, old_value, new_value, updated_at)
SELECT 'users', NEW.id,
field_name, old_val, new_val, NOW()
FROM (
SELECT 'phone' AS field_name, OLD.phone AS old_val, NEW.phone AS new_val
WHERE OLD.phone != NEW.phone OR (OLD.phone IS NULL) != (NEW.phone IS NULL)
UNION ALL
SELECT 'email', OLD.email, NEW.email
WHERE OLD.email != NEW.email OR (OLD.email IS NULL) != (NEW.email IS NULL)
) AS changes
WHERE changes.old_val IS NOT DISTINCT FROM OLD.phone OR changes.new_val IS NOT DISTINCT FROM NEW.phone;
关键点:
-
IS NOT DISTINCT FROM比!=更安全,能正确处理NULL比较 - 日志表
audit_log必须有索引:至少在(table_name, record_id, updated_at)上建联合索引,否则查历史变更会极慢 - 避免在触发器里做跨库写入或调用存储过程——MySQL 触发器不支持事务内嵌套事务,出错会导致主更新失败
触发器性能和权限踩坑清单
触发器是同步执行的,每行更新都会卡住主 SQL 直到日志写完。以下情况会让它变慢甚至拖垮业务:
- 日志表没主键或没索引,
INSERT变成全表扫描 - 在触发器里查其他大表(比如根据
user_id去departments表查部门名) - 用
CONCAT_WS拼接长文本再写入,触发器内字符串操作开销比想象中大 - 账号没对
audit_log表的INSERT权限——错误不报在触发器里,而是报ERROR 1442: Can't update table ... in stored function/trigger
测试时一定要用 EXPLAIN FORMAT=TREE 看触发器内联查询的执行计划,而不是只看主 SQL。
为什么不能只靠触发器做完整审计
触发器管不了这些场景:
-
UPDATE users SET phone = phone这种无实质变更的语句,OLD.phone = NEW.phone成立,触发器不会进日志 - 批量更新(
UPDATE ... LIMIT 1000)如果中途失败,部分行的日志已写入,但主事务回滚,日志就变成“幽灵记录” - DDL 变更(比如字段类型从
VARCHAR(50)改成VARCHAR(100))完全无法捕获
真正要满足合规审计要求,得配合 binlog 解析(如使用 mysqlbinlog 或 Debezium),触发器只适合轻量级、应用层可控的变更追踪。


















