级联删除不慢,慢的是单次删除大量数据;应分批删除而非禁用on delete cascade,每批次毫秒级完成,避免长事务导致wal暴涨、锁占用和主从延迟。

直接上结论:级联删除本身不慢,慢的是单次干掉几百万行;安全的前提不是关掉 ON DELETE CASCADE,而是控制它每次只删一小撮,并确保外键和索引能扛住。
为什么 ON DELETE CASCADE 会卡住数据库?
根本问题不在“级联”逻辑,而在 PostgreSQL/MySQL 执行时把整个级联链路塞进一个事务里。比如删 1 条 bills 记录触发删 5000 条 bill_items 和 200 条 payments,这没问题;但删 1600 万条 bills 就会让事务持续数小时——WAL 日志暴涨、pg_locks 长期占用、主从延迟飙升、连接池(如 HikariCP)标记连接为 broken(SQLSTATE 08006)。
这不是 SQL 写得不好,是 OLTP 系统根本不该跑这种长事务。
分批删除 + 级联 = 安全又有效
保留外键的 ON DELETE CASCADE 定义,但不用它一次性删光。改用带 LIMIT 的循环删除,让每批次都在毫秒级完成:
WITH batch AS ( SELECT id FROM bills WHERE expired_date::date
- 必须基于有索引的字段(如
id或expired_date)排序,否则LIMIT会强制全表扫描 - 若业务允许并发写入,加
FOR UPDATE SKIP LOCKED防止重复处理(PostgreSQL ≥ 9.5) -
ON DELETE CASCADE依然生效,子表删除由数据库自动完成,无需手写多条DELETE - 失败时只回滚当前批次,不影响已删数据,重试成本极低
什么时候不该用 ON DELETE CASCADE?
不是所有场景都适合开级联。以下情况建议改用 ON DELETE SET NULL 或逻辑删除:
- 主表记录被删后,子表数据仍有审计或分析价值(如金融订单与流水)
- 外键指向的是配置类表(如
status_types),删配置不应连带清空业务数据 - 级联深度超过 2 层(如
orders → order_items → item_logs),容易因中间表缺失索引导致某一层全表扫描 - 你无法控制删除入口——比如 ORM 自动生成的
DELETE不带WHERE,或前端传参过滤不严
真正容易被忽略的点:级联删除的性能瓶颈往往藏在子表缺少索引。比如 bill_items(bill_id) 没建索引,删一条 bills 就要扫全表找关联项——这时开 CASCADE 反而放大问题。











