必须使用AFTER触发器,审计表与业务表需同库同为InnoDB;AFTER INSERT/UPDATE/DELETE确保变更已落地,日志失败不影响主流程;字段比对需NULL安全,JSON类型优于TEXT,主键独立,changed_by由应用注入,触发器须异常捕获。

必须用 AFTER 触发器,且审计表和业务表得在同一个数据库、同为 InnoDB 引擎,否则写入失败或事务回滚会拖垮业务。
AFTER INSERT/UPDATE/DELETE 是唯一安全的触发时机
BEFORE 触发器在 INSERT 时拿不到 OLD,在 DELETE 时拿不到 NEW,UPDATE 时虽然两者都有,但一旦触发器内部出错(比如字段超长、JSON 构造失败),整个业务事务直接回滚——这不是审计,是阻断。AFTER 才能确保变更已落地,日志写入失败也不会影响主流程(当然你得处理好错误日志)。
- AFTER INSERT:只读
NEW.*,直接取NEW.id、NEW.email等即可 - AFTER UPDATE:必须逐字段判断是否真变化,不能无脑记录所有字段;用
做 NULL 安全比对,例如IF NOT (OLD.email NEW.email) - AFTER DELETE:只读
OLD.*,注意外键级联删除后OLD仍可访问,但字段值可能已被置空(取决于 ON DELETE 行为)
审计表结构不能照搬原表,字段类型和长度必须严格匹配
常见翻车点是 row_id 字段类型不一致:原表主键是 BIGINT UNSIGNED,审计表却定义成 INT,关联查询时隐式转换导致索引失效。另外,old_data 和 new_data 推荐用 JSON 类型(MySQL 5.7+),别用 TEXT 或 LONGTEXT——前者支持函数解析,后者不参与排序、易触发 max_allowed_packet 限制。
- 主键必须独立:
audit_id BIGINT AUTO_INCREMENT PRIMARY KEY -
table_name VARCHAR(64)、action_type ENUM('INSERT','UPDATE','DELETE'),别用VARCHAR存操作类型,避免索引失效 -
change_time DATETIME(3) DEFAULT NOW(3),不用CURRENT_TIMESTAMP,后者在复制场景下可能取从库时间 -
changed_by VARCHAR(128)必须由应用层通过SET @current_user_id = 'xxx'注入,USER()返回的是连接账户(如app@10.0.1.5),不是真实操作人
触发器里别拼整行 JSON,只存实际变化字段
把整行数据塞进 JSON_OBJECT('name', NEW.name, 'email', NEW.email, ...) 看似省事,但字段一多就慢,且 MySQL 的 JSON_OBJECT() 不支持动态键名,必须硬编码。更糟的是,遇到 BLOB、TEXT 或 NULL 时容易报 Truncated incorrect DOUBLE value 这类隐晦错误。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- INSERT:只记
NEW.id、NEW.created_at、@current_user_id和'INSERT' - UPDATE:用
IF NOT (OLD.col NEW.col)判断,只把变化字段构造成小 JSON 片段,再用JSON_MERGE_PATCH()合并 - DELETE:把关键字段转成 JSON 存进
old_data,例如JSON_OBJECT('id', OLD.id, 'email', OLD.email),别存全文本 - 所有触发器开头加
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION捕获异常,至少保证日志失败不中断业务
权限、索引和归档这些细节最容易被跳过
触发器以 DEFINER 权限运行,不是调用者权限。很多人只给业务表 INSERT 权限,忘了审计表也要授 INSERT。另外,没建索引的审计表查起来极慢,但建太多又拖慢写入——至少要有一个 INDEX idx_table_action_time (table_name, action_type, change_time) 支撑按表+操作+时间范围筛选。
- 定期归档旧数据:用
CREATE TABLE audit_log_202609 AS SELECT * FROM audit_log WHERE change_time +DELETE,别用TRUNCATE(它不走触发器,也绕过 binlog) - 禁用触发器内任何耗时操作:HTTP 请求、子查询、
SELECT ... FOR UPDATE、调用存储过程,这些都会让主 SQL 卡住 -
TRUNCATE TABLE永远不会触发任何触发器,这是 DDL 操作,审计盲区,需靠其他手段补漏(如 binlog 解析)
最常被忽略的其实是“谁改的”这个字段——USER() 和 CURRENT_USER() 都不可信,必须靠应用层显式传参;还有就是 DELIMITER 切换,不切就写不了多语句触发器,结果 CREATE TRIGGER 直接报语法错。










