必须用AFTER INSERT/UPDATE/DELETE触发器记日志,确保事务提交后再写入;日志表需含table_name、row_id、operation、JSON格式的old_value/new_value、created_at及user_id;禁用CONCAT拼接和触发器内复杂查询。

触发器里用 AFTER INSERT/UPDATE/DELETE 才能可靠记录变更
想靠触发器记日志,BEFORE 时机基本没用——比如 BEFORE UPDATE 时新旧值都还没落库,OLD 和 NEW 虽然可读,但事务若回滚,日志就和实际数据不一致了。必须用 AFTER,确保变更已提交再写日志表。
实操建议:
-
AFTER INSERT:只读NEW,记录新增行完整字段(或关键字段) -
AFTER UPDATE:同时读OLD和NEW,只记录真正变化的字段,避免冗余(比如UPDATE users SET name='a' WHERE id=1,但 name 原值就是 'a',就不该记) -
AFTER DELETE:只读OLD,注意被删行已不存在,必须此刻存快照
日志表结构要包含足够上下文,别只存变更字段
光记“name 从 a 改成 b”没意义,查问题时根本不知道是谁改的、什么时候改的、从哪来的请求。日志表至少得有这些字段:
-
id(自增主键) -
table_name(如'users'),方便跨表统一查日志 -
row_id(被操作行的主键值,类型要匹配原表,比如INT或BIGINT) -
operation('INSERT'/'UPDATE'/'DELETE') -
old_value和new_value(用JSON类型存字段级差异,比拼大文本更易解析) -
created_at(用NOW(),别依赖应用层传时间) -
user_id(如果应用层能透传当前操作人 ID,就存;否则留空或设为0,别硬填CURRENT_USER()——它返回数据库账号,不是业务用户)
JSON_OBJECT() 是构造日志值最稳妥的方式
有人用 CONCAT 拼字符串记变更,结果遇到单引号、换行、NULL 就崩。MySQL 5.7+ 直接用 JSON_OBJECT() 安全又清晰:
SET @old = JSON_OBJECT('id', OLD.id, 'name', OLD.name, 'email', OLD.email);
SET @new = JSON_OBJECT('id', NEW.id, 'name', NEW.name, 'email', NEW.email);
注意点:
- 字段名必须是字符串字面量(
'name'),不能写name(会当变量) -
NULL值会被自动转成 JSON 的null,不用额外处理 - 如果只记部分字段(比如只关心
status变更),就只往JSON_OBJECT()里放那几个,别全量塞
触发器里别调用存储过程或复杂查询
触发器执行在主 DML 事务内,任何慢操作都会拖慢业务 SQL。常见翻车点:
- 在触发器里查其他大表做关联(比如根据 user_id 查用户名再写进日志)——锁表风险高,还可能死锁
- 调用含事务控制的存储过程(
START TRANSACTION等),会报错Can't execute statement in trigger - 写日志表时用
INSERT ... SELECT多行插入,不如单行INSERT稳定
真正要关联信息,应该由应用层在写主表前查好,一并传入(比如把操作人昵称存在注释字段或临时表),或者事后用异步任务补全日志详情。
触发器不是万能日志代理,它只负责“这一刻发生了什么”,别让它承担本该由应用或 ETL 做的事。


















