审计必须分离存储,用独立审计表+触发器捕获字段级变更,避免覆盖历史、破坏主键或引发死锁;需建复合索引并定期归档。

为什么不能直接在原表加操作时间字段做审计
加 updated_at 或 last_modified_by 只能记录整行最后变更,无法回答“email 字段上周被谁改成什么值”这种问题。原表不是审计表,改一次覆盖一次,历史就丢了。
常见错误是试图用触发器往原表写多条记录——这会破坏主键约束、引发死锁,且让业务查询变慢。审计必须分离存储。
用独立审计表 + AFTER UPDATE 触发器捕获字段变化
核心思路:每张需审计的业务表,配一张结构固定的审计表,由触发器在每次更新后,只把真正变过的字段写进去。
关键点:
- 审计表字段至少包含:
audit_id(自增)、table_name、record_id(关联原表主键)、field_name、old_value、new_value、updated_at、updated_by - 触发器里用
OLD.field和NEW.field逐字段比对,跳过 NULL/NULL 或值相等的情况 - 避免审计敏感字段(如密码):触发器中显式排除
password_hash等列 - PostgreSQL 示例片段:
IF OLD.email IS DISTINCT FROM NEW.email THEN<br> INSERT INTO audit_log (...) VALUES ('users', OLD.id, 'email', OLD.email, NEW.email, ...);<br>END IF;
MySQL 8.0+ 用 JSON_CONTAINS + GENERATED COLUMN 做轻量级字段变更检测
如果不想写大量 IF 判断,又用的是 MySQL 8.0+,可以利用生成列 + JSON 函数压缩逻辑。
做法:
- 在触发器中把
OLD和NEW各转成 JSON:JSON_OBJECT('email', OLD.email, 'status', OLD.status) - 用
JSON_CONTAINS或JSON_EXTRACT对比字段值,例如:JSON_EXTRACT(old_json, '$.email') != JSON_EXTRACT(new_json, '$.email') - 注意:JSON 转换有性能开销,单次更新字段数超过 10 个时,显式 IF 效率更高
- 不推荐在审计表里存完整 JSON,因为后续查“所有被改成 active 的 status 变更”会无法走索引
审计日志查询慢?给 audit_log 加复合索引并定期归档
审计表增长极快,没索引时查某用户某字段变更可能要全表扫几百万行。
必须建的索引:
-
INDEX idx_table_record_field (table_name, record_id, field_name)—— 支撑按记录查变更 -
INDEX idx_field_time (field_name, updated_at)—— 支撑按字段查时间范围变更
归档建议:
- 用
PARTITION BY RANGE COLUMNS(updated_at)按月分区(MySQL)或CREATE TABLE audit_log_202406手动分表(兼容性更好) - 保留最近 6 个月在线,更早数据移到冷库存储,应用层路由查询
- 别依赖
DELETE FROM audit_log WHERE updated_at —— 大删易锁表,用DROP PARTITION或TRUNCATE TABLE更安全
字段级审计真正的复杂点不在写入,而在查。一个 WHERE field_name = 'price' AND old_value > new_value 的查询,如果没有对应索引,再小的系统也扛不住三个月数据。










