必须用before update/delete触发器,通过old取变更前数据插入结构一致的备份表;禁止反查原表或更新同表,否则触发error 1442;需逐字段判断变更、加索引防性能退化,并排除时间戳等噪音字段。

触发器里不能直接 INSERT INTO 目标表自身
想在 UPDATE 前把旧数据存到备份表?别在触发器里写 INSERT INTO orders SELECT * FROM orders WHERE id = NEW.id——这会触发死循环或报错 ERROR 1442: Can't update table 'orders' in stored function/trigger because it is already used by statement which invoked this stored function/trigger。MySQL 禁止在触发器中修改(包括 SELECT FOR UPDATE、INSERT INTO 同表等)当前正在被操作的表。
正确做法是:提前建好结构一致的备份表(如 orders_backup),并在触发器中只对它做 INSERT,且必须用 OLD 关键字取修改前的值:
CREATE TRIGGER backup_orders_before_update
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
INSERT INTO orders_backup
VALUES (OLD.id, OLD.user_id, OLD.amount, OLD.status, OLD.updated_at, NOW());
END;
-
OLD只在BEFORE UPDATE和BEFORE DELETE中可用,NEW用于INSERT或UPDATE的新值 - 备份表字段顺序、类型必须和原表严格一致,否则
VALUES会报错Column count doesn't match value count - 如果原表有自增主键,备份表对应字段不能设
AUTO_INCREMENT,否则INSERT ... VALUES (OLD.id, ...)会冲突
DELETE 场景下必须用 BEFORE 而非 AFTER
AFTER DELETE 触发器里 OLD 仍可访问,但此时原行已物理删除,无法再关联查其他表补全信息(比如想把用户昵称一起记进备份)。更关键的是:如果备份逻辑依赖原表外键关联的数据(例如订单删了,还想把对应用户姓名存进备份),AFTER 阶段可能因级联删除或事务隔离导致查不到数据。
所以涉及关联字段补充的备份,一律用 BEFORE DELETE:
CREATE TRIGGER backup_orders_before_delete
BEFORE DELETE ON orders
FOR EACH ROW
BEGIN
INSERT INTO orders_backup
SELECT OLD.id, u.nickname, OLD.amount, OLD.status, OLD.updated_at, NOW()
FROM users u
WHERE u.id = OLD.user_id;
END;
- 这里用
SELECT ... FROM users是安全的,因为发生在删除前,关联数据一定存在 - 若
users表没加索引在id字段上,这个 JOIN 会拖慢删除速度,线上慎用 - 注意
BEFORE DELETE中不能修改OLD的值(不像BEFORE UPDATE可改NEW),只能读
UPDATE 时避免备份重复记录
一个订单被频繁更新,每次都会触发备份,很快备份表就膨胀。常见误操作是没加条件判断,比如:
-- ❌ 错误:不管字段是否真变了,都备份 INSERT INTO orders_backup VALUES (OLD.id, OLD.amount, NOW());
应该只在关键字段变更时才备份。MySQL 不支持直接比较两个记录,得逐字段判断:
-- ✅ 正确:只当 amount 或 status 改了才备份
IF OLD.amount != NEW.amount OR OLD.status != NEW.status THEN
INSERT INTO orders_backup VALUES (OLD.id, OLD.amount, OLD.status, NOW());
END IF;
-
!=对NULL不生效,要用IS DISTINCT FROM(MySQL 8.0.17+)或手写(OLD.x IS NULL) != (NEW.x IS NULL) OR OLD.x != NEW.x - 字符串比较注意 collation,比如
utf8mb4_0900_as_cs区分大小写,_ci不区分,可能导致误判 - 时间字段(如
updated_at)常被业务自动更新,如果只是时间戳变,不该触发备份——得排除这类“噪音字段”
备份表没加索引会拖垮主表性能
触发器执行是同步阻塞的,INSERT INTO orders_backup 如果没索引,随着备份数据增多,单次插入从毫秒级涨到秒级,用户端就会感知到 UPDATE 变慢。尤其当备份表达到百万级,没索引的 INSERT 可能锁表几秒。
至少要给备份表加上这两个索引:
ALTER TABLE orders_backup ADD INDEX idx_backup_origin_id (origin_id); ALTER TABLE orders_backup ADD INDEX idx_backup_created_at (created_at);
-
origin_id是你存的原表主键,方便按源记录查所有历史版本 -
created_at用于按时间范围清理旧备份(比如只保留 90 天),没索引的话DELETE FROM orders_backup WHERE created_at 会全表扫描 - 别忘了定期归档,备份表不清理,磁盘和查询性能迟早崩
真正麻烦的不是写触发器,是备份后的数据怎么查、怎么清理、怎么保证和主表事务一致——这些不在触发器里,但漏掉任何一个,备份就变成负债。











