触发器应只监听业务定义的敏感字段(如email、phone、id_card),用IS DISTINCT FROM或显式NULL判断安全比对;INSERT/UPDATE/DELETE需分三个独立触发器处理;audit_log表宜用JSON存变更字段,避免宽表;需注意性能、权限及ORM批量操作绕过问题。

触发器该监听哪些字段变化
敏感字段修改追踪的核心不是“所有字段都记”,而是精准捕获业务定义的敏感列,比如 users 表中的 email、phone、id_card。直接在触发器里写 OLD.email != NEW.email 是常见做法,但要注意:NULL 值比较会返回 UNKNOWN,导致条件失效。必须显式用 IS DISTINCT FROM(PostgreSQL)或 (OLD.col IS NULL) != (NEW.col IS NULL) OR OLD.col != NEW.col(MySQL/SQL Server)来安全比对。
实际部署前建议先查一遍历史数据中这些字段的 NULL 分布,避免因大量 NULL 导致触发器逻辑漏判。
INSERT/UPDATE/DELETE 三种操作要分开处理
一个触发器不能同时可靠覆盖三类操作——尤其是 DELETE:没有 NEW 行,只能记录 OLD.id 和操作类型;而 INSERT 没有 OLD,无法做“变更对比”。强行合并会导致逻辑臃肿、难调试。推荐拆成三个独立触发器:
-
BEFORE UPDATE ON users FOR EACH ROW:只做字段差异判断 + 写入 audit_log -
AFTER INSERT ON users FOR EACH ROW:记录插入动作(可选,视审计要求) -
AFTER DELETE ON users FOR EACH ROW:记录删除行为,含被删行主键和时间
MySQL 不支持 BEFORE DELETE 中访问 OLD 的完整字段值(部分版本限制),此时只能依赖 AFTER DELETE 并提前在临时表缓存关键字段。
audit_log 表结构设计要点
别用宽表存所有字段快照,既浪费空间又拖慢写入。推荐最小化设计:id、table_name、row_id(被修改行主键)、operation('U'/'I'/'D')、changed_fields(JSON 字符串,如 {"email": ["old@x.com", "new@x.com"]})、updated_by(从 SESSION_USER 或应用传入的 context 变量获取)、created_at。其中 changed_fields 字段用 JSON 而非多行记录,能避免一对多关联查询开销,也方便应用层解析。
注意:MySQL 5.7+ 支持 JSON 类型并可建虚拟列索引;PostgreSQL 直接用 jsonb 更高效。若用 TEXT 存 JSON,请确保应用写入时已格式化,避免触发器内做字符串拼接引发 SQL 注入风险。
触发器性能与权限隐患
触发器在事务内同步执行,一旦 audit_log 表写入慢(比如没建索引、磁盘 I/O 高),会拖垮主业务更新响应。务必给 audit_log.created_at 加索引,并定期归档旧数据。另外,触发器运行时默认拥有定义者的权限,如果用高权限账号(如 root)创建,可能绕过行级安全策略。生产环境应单独建 auditor 用户,仅授予对 audit_log 的 INSERT 权限,再用该用户创建触发器。
还有一个容易被忽略的点:某些 ORM(如 Django ORM)批量更新(QuerySet.update())会绕过触发器——因为不经过单行 INSERT/UPDATE 语句。这类场景需额外在应用层补日志,或改用存储过程封装。

















