先说结论:可以用触发器把 INSERT 数据同步到审计表,但必须用 AFTER INSERT,并且审计表的字段顺序、类型、字符集得和源表严格一致。否则,轻则写入错位、数据截断,重则直接报错、丢数据。下面把这些坑一个一个掰开说。
能,但必须用 AFTER INSERT,且审计表字段顺序、类型、字符集要和源表严格一致;否则写入会错位、截断甚至报错。
为什么不能用 BEFORE INSERT 同步到审计表?
问题出在时序上:BEFORE INSERT 阶段,主表记录还没落盘,自增主键(比如 id)可能还没生成。这时候你往审计表里插 NEW.id,得到的是一堆 NULL 或者默认值,数据对不上。
- MySQL 里,
NEW.id在BEFORE阶段对自增列根本不靠谱,除非你显式赋了值。 - 只有切换到
AFTER INSERT,才能确保NEW包含的是完整、已确认的行数据。 - PostgreSQL 虽然允许在
BEFORE里访问NEW,但做数据同步这种场景,大家还是习惯用AFTER,避免逻辑上绕来绕去。
INSERT INTO audit SELECT NEW.* 为什么经常失败?
这种写法看着省事,实际上埋着雷。只要审计表比源表多一个字段(比如加了 op_type、op_time),或者字段顺序对不上,立刻给你来一句 Column count doesn't match value count。
- 正确做法是显式列出目标列:
INSERT INTO audit (id, name, op_type, op_time) VALUES (NEW.id, NEW.name, 'INSERT', CURRENT_TIMESTAMP)。 - 字符集和排序规则不一致也会翻车——源表用
utf8mb4_unicode_ci,审计表用utf8mb4_general_ci,触发器里直接报Illegal mix of collations。 - 如果审计表有个
NOT NULL字段,但你在VALUES里漏掉了它,插入照样失败。
如何安全透传操作人 ID,而不是数据库连接用户?
CURRENT_USER() 返回的是 app@10.0.1.% 这种登录账号,对业务审计来说基本没用。真正的操作人得由应用层带进来。
- 应用在发 SQL 之前先设个会话变量:
SET @current_app_user = 12345; - 触发器里直接读:
op_user = @current_app_user(注意:不能在BEFORE里用:=给NEW赋值,除非审计表字段和源表完全一致)。 - 千万别用
USER()或SYSTEM_USER(),它们反映的是 TCP 连接身份,跟业务主体八竿子打不着。 - 更健壮的做法是应用在 SQL 注释里埋信息,比如
INSERT /* user_id=12345 */ INTO orders...,但触发器解析不了这个——得靠 ProxySQL 或者应用日志来兜底。
同步量大时,触发器会拖慢主表写入吗?
会,而且非常明显。触发器是同步阻塞的——主表的 INSERT 必须等审计写完才能返回。一个慢查询、一次锁等待、一次跨库写入,都能把业务卡住。
- 审计表上尽量少建索引,非必要不建,只保留按时间范围查询用的复合索引,比如
(op_time, table_name)。 - 避免在触发器里调用
UUID()、JSON_OBJECT()这些函数——MySQL 5.7 不支持,8.0 支持但性能损耗很高。 - 如果日均审计量达到百万级别,建议放弃触发器,改用应用层双写,或者解析 binlog 异步补全。
回头总结一下:真正难搞的不是触发器语法本身,而是字段一致性、时区对齐、操作人透传这三个点。任何一个漏掉,日志就不可信。性能问题往往上线之后才暴露,到那时再改架构,代价就大了。
