触发器中禁止使用COMMIT、ROLLBACK等事务控制语句,须依赖外层事务兜底;应直接用OLD(MySQL/PostgreSQL)或deleted表(SQL Server)获取旧值插入历史表,字段不匹配时需显式列名。

触发器里不能用事务控制语句
想在 UPDATE 或 DELETE 时自动存一份旧数据到历史表,很多人第一反应是加 BEGIN TRANSACTION 和 COMMIT。但绝大多数数据库(MySQL 5.7+、PostgreSQL、SQL Server)明确禁止在触发器中执行 COMMIT、ROLLBACK 或显式事务控制语句——这会导致错误如 ERROR 1442 (HY000): Can't update table 'xxx' in stored function/trigger because it is already used by statement which invoked this stored function/trigger。
正确做法是把快照逻辑当作 DML 操作本身的一部分:直接 INSERT INTO history_table SELECT ... FROM OLD,靠外层事务统一兜底。
- MySQL 触发器中用
OLD.*获取被修改/删除行的原始值;PostgreSQL 用OLD记录变量;SQL Server 用deleted临时表 - 不要在触发器里调用存储过程再做插入——除非该过程只做纯 INSERT,且不涉及当前表
- 若历史表字段比原表多(比如加了
op_type、updated_at),需显式列出列名,避免因顺序或数量不匹配报错
INSERT BEFORE 还是 AFTER?关键看是否依赖新主键
对新增记录做快照意义不大(历史表通常存变更/删除),但如果你要记录“某条记录首次创建时的完整上下文”,就得用 BEFORE INSERT 触发器——因为某些场景下主键是自增的,AFTER 才能拿到真实值;而 BEFORE 只能拿到默认值或 NULL。
更常见的是为 UPDATE 和 DELETE 建 AFTER 触发器,确保 OLD 数据稳定可用:
- MySQL 中
BEFORE UPDATE的OLD是修改前的,AFTER UPDATE的OLD一样可用,但推荐用AFTER避免和业务逻辑中的赋值冲突 - PostgreSQL 的
BEFORE触发器可修改NEW,但不能读写OLD(只读);AFTER则两者都只读,更适合只做日志 - 如果历史表主键依赖原表主键(比如用
id+version联合唯一),必须等原表 INSERT 完成后再插入历史表,否则可能主键冲突或查不到刚生成的 ID
MySQL 中 INSERT … SELECT 多行更新的坑
当一条 UPDATE 影响多行(例如 UPDATE users SET status = 'archived' WHERE created_at ),对应触发器会为每一行单独执行一次,不是批量插入。这意味着:
-
INSERT INTO user_history SELECT OLD.* FROM DUAL写法在单行时有效,但多行场景下必须用FOR EACH ROW+ 单行INSERT - 不要试图在触发器里用
INSERT INTO ... SELECT从OLD查多行——OLD不是表,只是当前行的别名,不支持集合操作 - 如果历史表有自增主键,且你希望每条快照带时间戳,直接用
NOW()或CURRENT_TIMESTAMP,别依赖函数返回值在触发器外计算 - 大表批量删改时,触发器会显著拖慢执行速度——每行都额外走一次 INSERT,IO 和锁开销翻倍;线上慎用,建议改用应用层双写或定时归档
PostgreSQL 中用 row_to_json() 快速落库但要注意类型
比起逐字段插入,PostgreSQL 支持用 row_to_json(OLD) 把整行转成 JSON 存进 jsonb 字段,省去建宽表的麻烦。但要注意:
-
row_to_json()不包含 OID、系统字段(如tableoid),也不处理bytea类型——会转成 base64 字符串,体积膨胀 - 如果原表有数组、hstore、JSON 类型字段,
row_to_json()能保留结构;但 MySQL 没原生等价函数,得拼字符串或用应用层处理 - JSON 方案适合快速原型或审计类需求,不适合后续按字段查(比如“查所有 status 从 active 变为 inactive 的快照”),这时还是老老实实建结构化历史表
- 记得给
jsonb字段加 GIN 索引,否则WHERE data->>'status' = 'inactive'会全表扫描
实际部署时最容易被忽略的点是:触发器不会复制外键约束、索引或权限设置到历史表,每次新建历史表后得手动补上;另外,ALTER TABLE 原表结构后,必须同步检查并更新触发器里的字段列表,否则 OLD.xxx 引用会直接报错。

















