必须用BEFORE DELETE,因为只有此时OLD.*是可靠、只读、事务一致的快照;AFTER中原行已从聚簇索引移除,反查原表易为空或死锁。

能,但必须用 BEFORE DELETE,且只能通过 OLD 引用被删行;AFTER DELETE 里查不到原数据,强行 SELECT 可能读空或引发死锁。
为什么必须用 BEFORE DELETE 而不是 AFTER
BEFORE DELETE 是唯一能稳定拿到完整旧记录的时机。OLD.* 在此时是可靠、只读、事务内一致的快照。一旦进入 AFTER DELETE,原行已从聚簇索引中移除,哪怕加 SELECT ... FOR UPDATE 也极可能触发间隙锁冲突或返回空结果——这不是 bug,是 InnoDB MVCC 和锁机制的正常表现。
常见错误是写成:
CREATE TRIGGER backup_after_del AFTER DELETE ON user FOR EACH ROW BEGIN INSERT INTO user_history SELECT * FROM user WHERE id = OLD.id; -- ❌ 大概率查不到 END;
这在高并发下尤其危险,还可能拖慢主删除操作。
备份语句怎么写才安全可靠
直接用 INSERT ... SELECT 或逐字段插入都行,但必须基于 OLD,不能反查原表。推荐显式列出字段,避免因表结构变更导致触发器失效:
- 字段顺序、类型、数量必须与备份表严格一致;
- 如果备份表多了
deleted_at字段,就手动补上NOW(); - 不要用
SELECT *,哪怕原表和备份表当前字段完全一样——下次加字段就崩; - 确保备份表主键与原表一致(如都是
id),否则后续按主键还原会出错。
示例:
DELIMITER $$
CREATE TRIGGER user_del_backup BEFORE DELETE ON user FOR EACH ROW
BEGIN
INSERT INTO user_history (
id, name, email, created_at, updated_at, deleted_at
) VALUES (
OLD.id, OLD.name, OLD.email, OLD.created_at, OLD.updated_at, NOW()
);
END$$
DELIMITER ;容易被忽略的兼容性与维护风险
MySQL 8.0+ 对触发器的 DEFINER 更严格,升级前要检查是否用了不存在的用户;另外,如果原表是分区表,触发器仍生效,但备份表最好也用 InnoDB 引擎并关闭分区——分区表上的触发器行为在不同 MySQL 版本间有细微差异,不值得为备份引入额外不确定性。
真正麻烦的点不在语法,而在于:备份表膨胀后没清理策略、没加索引导致 SELECT ... FROM user_history WHERE id = ? 变慢、以及误把日志表当“可回滚快照”——它只存删前状态,没存事务上下文,也没自动处理外键级联删除的关联行。


















