必须用BEFORE DELETE触发器手动归档子表数据,因ON DELETE CASCADE直接物理删除、无法备份;需先禁用外键级联,再通过OLD.id精准查询并INSERT子表到结构一致的归档表,失败时抛异常中断事务确保原子性。

触发器里不能用 DELETE CASCADE
级联删除(ON DELETE CASCADE)本身不支持归档,它直接物理删掉子表记录,没机会存档。想安全归档,必须绕开外键级联,改用 BEFORE DELETE 或 AFTER DELETE 触发器拦截主表删除动作,手动把子表数据 INSERT 到归档表,再删原记录。
常见错误是写成 AFTER DELETE 再查子表——但此时外键约束若已启用 CASCADE,子表行可能已被删光,查不到数据;所以必须关掉外键级联,全由触发器接管。
- 主表外键定义中移除
ON DELETE CASCADE - 归档表结构需与子表完全一致(含索引、约束可酌情简化)
- 触发器内操作需在同一个事务中,确保归档成功才允许主表删除
BEFORE DELETE 触发器中如何安全复制子表数据
BEFORE DELETE 是唯一能“预读”待删主键并关联子表的时机。触发器里用 OLD.id 查子表,INSERT 到归档表,再让原 DELETE 继续执行。注意:不能在触发器里直接 DELETE 子表,否则会和外键约束冲突或引发递归。
PostgreSQL 示例:
CREATE OR REPLACE FUNCTION archive_child_on_parent_delete() RETURNS TRIGGER AS $$ BEGIN INSERT INTO orders_archive SELECT * FROM orders WHERE customer_id = OLD.id; RETURN OLD; -- 允许主表删除继续 END; $$ LANGUAGE plpgsql; <p>CREATE TRIGGER trg_archive_orders BEFORE DELETE ON customers FOR EACH ROW EXECUTE FUNCTION archive_child_on_parent_delete();
- MySQL 不支持
BEFORE DELETE中对同库其他表做 DML,得改用AFTER DELETE+ 显式事务 + 临时表暂存OLD.id - SQL Server 可用
INSTEAD OF DELETE更精准控制流程 - 所有数据库都要给归档表加
archived_at TIMESTAMP DEFAULT NOW()字段,否则无法区分归档批次
归档表写入失败时如何阻止主表删除
归档失败必须中断整个删除事务,否则主表删了、子表没归档,数据就丢了。不同数据库处理方式不同:
- PostgreSQL / SQL Server:触发器内抛异常(
RAISE EXCEPTION或THROW),自动回滚 - MySQL:在存储过程里用
SIGNAL SQLSTATE '45000'中断,但需确保触发器是BEFORE类型且未被禁用 - 务必测试磁盘满、归档表只读、字段类型不匹配等场景,这些都会让 INSERT 失败
别依赖应用层重试——触发器事务一旦提交,主表删除不可逆。
性能与锁竞争问题怎么避开
归档操作本质是额外 I/O 和锁,尤其子表大时,INSERT ... SELECT 会锁住整张子表,阻塞其他业务查询。真实线上环境容易卡住。
- 避免在触发器里做复杂 JOIN 或子查询,只查目标子表且用
WHERE parent_id = OLD.id精准定位 - 归档表建议关闭非必要索引,插入后再重建(但要注意归档后查询性能)
- 高频删除场景下,考虑异步归档:触发器只写一条日志到轻量队列表,由后台任务消费归档,但需接受短暂窗口期数据未归档
- Oracle 用户注意:
AUTONOMOUS_TRANSACTION会脱离主事务,导致归档成功但主删失败时数据不一致,禁用
最麻烦的是跨分片或读写分离架构下,归档表可能不在同一实例,触发器根本没法跨库操作——这时候只能放弃触发器方案,改用应用层双写或 CDC 工具捕获变更。

















