在关系型数据库设计中,父表与子表通过外键形成一对多关联。当业务需要删除父表记录,同时又不能丢失子表的历史数据时,仅靠应用代码去先查子表再备份往往存在事务不一致的风险。利用数据库触发器,可以让备份动作和删除动作处在同一个事务里,从而保证数据完整性。

一、核心原理与表结构设计
触发器的本质是在指定的表上发生 INSERT、UPDATE 或 DELETE 事件前后,由数据库自动执行的一段存储过程。要实现删除父表时自动备份子表,我们通常使用 BEFORE DELETE 或 AFTER DELETE 触发器。BEFORE DELETE 的优势在于如果备份插入失败,整个删除会被回滚;AFTER DELETE 则适用于父表已删除、再利用行级日志处理的场景。这里以 BEFORE DELETE 为例,确保原子性。
首先,我们需要一张与子表结构基本一致的备份表。为便于追溯,备份表应增加备份时间和原父表主键字段。假设父表为 orders,子表为 order_items,备份表可设计为 order_items_backup。下面给出建表语句示例:
-- 父表 CREATE TABLE orders ( id INT PRIMARY KEY, customer_name VARCHAR(100), created_at DATETIME ); -- 子表 CREATE TABLE order_items ( id INT PRIMARY KEY, order_id INT, product_name VARCHAR(100), quantity INT, FOREIGN KEY (order_id) REFERENCES orders(id) ); -- 备份表 CREATE TABLE order_items_backup ( backup_id INT AUTO_INCREMENT PRIMARY KEY, item_id INT, order_id INT, product_name VARCHAR(100), quantity INT, deleted_at DATETIME, backup_time DATETIME );
上述结构中,order_items_backup 通过 backup_time 记录备份发生时间,deleted_at 可存放父表 orders 的删除时间(若需要)。这种结构在后期排查“某次删除带来了哪些子表数据流失”时非常直观。
值得注意的是,备份表不需要外键约束回 orders,否则父表删除会因外键失效而报错。同时备份表的索引应根据查询习惯建立,例如对 order_id 和 backup_time 建联合索引,能加快按删除批次回溯的速度。
二、编写触发器完成级联备份
在 MySQL 中,使用 CREATE TRIGGER 语法定义触发器。我们需要捕获父表 orders 的删除事件,并将对应的 order_items 行复制到备份表。由于触发器是按行触发的,OLD 关键字代表即将被删除的父表行,利用 OLD.id 即可定位子表数据。
DELIMITER $$
CREATE TRIGGER trg_orders_before_delete
BEFORE DELETE ON orders
FOR EACH ROW
BEGIN
INSERT INTO order_items_backup (
item_id,
order_id,
product_name,
quantity,
deleted_at,
backup_time
)
SELECT
id,
order_id,
product_name,
quantity,
NOW(),
NOW()
FROM order_items
WHERE order_id = OLD.id;
END$$
DELIMITER ;
这段代码中,FOR EACH ROW 表示父表每删除一行,触发器就执行一次。SELECT ... FROM order_items WHERE order_id = OLD.id 会把该订单下的所有子表记录查出,并插入备份表。由于整个操作在父表 DELETE 的事务内,若备份插入因磁盘满等原因失败,父表删除也会回滚。
如果数据库是 PostgreSQL,写法略有不同,它使用函数与触发器分离的模式。示例如下:
CREATE OR REPLACE FUNCTION backup_order_items() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO order_items_backup (
item_id, order_id, product_name, quantity, deleted_at, backup_time
)
SELECT id, order_id, product_name, quantity, NOW(), NOW()
FROM order_items
WHERE order_id = OLD.id;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_orders_before_delete
BEFORE DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION backup_order_items();
两种数据库在实现思路上完全一致:都是借助 OLD 伪记录拿到父表主键,再驱动子表备份。区别在于语法组织和函数封装方式。
三、方案优缺点与性能考量
使用触发器做级联备份的最大优点是透明性和强一致性。业务代码无需感知备份逻辑,所有删除操作无论来自后台脚本、命令行还是 API,都会触发备份。相比之下,应用层备份容易因代码分支遗漏或异常捕获不当而丢数据。
但触发器也存在缺点。首先是性能开销:删除父表若关联大量子表行,触发器内的 INSERT SELECT 会拉长事务时间,可能导致锁等待。对于批量删除父表场景,建议分批删除,或改用异步归档任务。其次是维护复杂度,触发器逻辑隐藏在数据库内,新人接手时如果不查看库结构容易忽略备份行为。因此应在数据库文档中明确标注触发器用途。
| 方案 | 一致性 | 性能影响 | 维护成本 |
|---|---|---|---|
| 触发器级联备份 | 强一致(同事务) | 随子表量线性增加 | 中(需查库知逻辑) |
| 应用层先查后插 | 弱(跨事务风险) | 可控但易漏写 | 低(代码可见) |
| 定时快照备份 | 弱(时间点间隙丢) | 低峰期执行 | 低 |
从上表可以看出,触发器方案在一致性上明显占优,适合对审计要求高的系统,例如电商订单、医疗记录等。若子表日均删除量极小,触发器带来的性能损耗可以忽略。
四、常见误区与注意事项
一个常见误区是认为 AFTER DELETE 比 BEFORE DELETE 更安全。实际上,如果备份表插入失败,AFTER DELETE 中父表记录已经消失,数据无法回滚,反而造成永久丢失。因此备份场景优先选 BEFORE DELETE。另一个误区是在触发器里调用 UUID 或 RAND 等非确定性函数,这会导致主从复制时备库执行结果不一致,应统一使用 NOW 或数据库提供的稳定事务时间函数。
此外,如果父表本身使用软删除(如 status 字段标记),则不需要触发器备份,只需在查询时过滤状态即可。触发器只适用于物理删除场景。当业务演进到需要分库分表时,触发器无法跨库生效,此时应迁移到应用层或 binlog 订阅方案完成备份。
总结来说,编写 SQL 触发器完成级联备份是一种低侵入、高可靠的数据库层解决方案。只要理清父表与子表的关联键,建好带时间标记的备份表,再用 OLD 引用驱动插入,就能在删除发生时自动留痕。