mysql审计日志触发器必须用after而非before,以确保真实记录已发生的变更;日志表须与业务表同引擎(推荐innodb),字段用longtext并显式处理null;获取用户/ip应优先由应用层传入变量;update触发器需逐字段比对,跳过无变更或自动更新字段。

MySQL触发器审计日志必须用AFTER而非BEFORE
审计日志的核心诉求是“真实记录已发生的变更”,所以必须在语句执行成功后写日志。若用BEFORE INSERT/UPDATE/DELETE,事务回滚时日志却已写入,导致日志与实际数据不一致;更严重的是,BEFORE中无法读取NEW.id(INSERT)或OLD.id(DELETE)在某些MySQL版本中会报Unknown column 'OLD.id' in 'field list'错误。
实操建议:
- 统一使用
AFTER INSERT、AFTER UPDATE、AFTER DELETE - 日志表必须与业务表同引擎(推荐
InnoDB),否则跨引擎事务可能失败 - 避免在触发器里做复杂逻辑或调用存储过程——会显著拖慢主DML性能
审计日志表结构要兼容NULL和长文本字段
业务表字段可能为NULL,也可能含TEXT、JSON等大类型,直接拼接进日志表容易因长度超限或类型不匹配报错,典型错误如:Data too long for column 'old_value' at row 1。
实操建议:
- 日志表对应字段用
LONGTEXT存变更前/后值,不用VARCHAR(255) - 对
NULL值显式转成字符串'NULL',避免CONCAT()整个结果为NULL - 关键字段如
table_name、operation、user_host设为NOT NULL并加索引,方便后续查
示例建表语句片段:
CREATE TABLE audit_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
table_name VARCHAR(64) NOT NULL,
operation ENUM('INSERT','UPDATE','DELETE') NOT NULL,
pk_value VARCHAR(255) NOT NULL,
old_value LONGTEXT,
new_value LONGTEXT,
user_host VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY idx_table_op_time (table_name, operation, created_at)
);
触发器里获取当前用户和客户端IP不能只靠USER()
USER()返回的是“账号@主机名”(如'app_user@10.20.30.40'),但生产环境常走中间件或代理,真实客户端IP会被掩盖;且部分DBA会复用账号,仅靠账号无法区分操作人。
实操建议:
- 优先用
SYSTEM_USER()或CURRENT_USER()辅助判断,但最可靠的是应用层传入上下文(如通过SET @audit_user = 'alice') - 若必须从连接信息提取,在触发器中用
SUBSTRING_INDEX(USER(), '@', -1)取IP段,再结合INFORMATION_SCHEMA.PROCESSLIST反查(需PROCESS权限,不推荐) - 更稳妥做法:在应用代码里显式写入
@audit_user和@audit_client_ip,触发器直接读变量
UPDATE触发器必须逐字段比对,不能无差别记录整行
审计不是备份,重点是“改了什么”。如果AFTER UPDATE触发器把OLD.*和NEW.*全塞进日志,不仅浪费空间,还让日志难以阅读。更糟的是,当某字段没变但类型为TIMESTAMP或带ON UPDATE CURRENT_TIMESTAMP时,OLD.updated_at != NEW.updated_at恒成立,造成大量无效日志。
实操建议:
- 用
CASE WHEN OLD.col1 != NEW.col1 THEN CONCAT('"col1": "', OLD.col1, '" → "', NEW.col1, '"') ELSE NULL END方式逐字段判断 - 对JSON字段用
JSON_CONTAINS()或JSON_EXTRACT()做细粒度比对(MySQL 8.0+) - 跳过自增ID、时间戳自动更新字段,除非业务明确要求追踪它们
OLD/NEW在不同SQL模式下的行为差异,比如STRICT_TRANS_TABLES开启时,空字符串转数值可能报错并中断触发器执行。











