直接delete from parent where id in (...)会因外键约束失败,需先递归获取并删除所有子节点或按依赖逆序分步删除。

为什么直接 DELETE FROM parent WHERE id IN (...) 会失败
因为外键约束存在,子表记录没被清理前,父表记录无法删除。常见报错是 ERROR 1451 (HY000): Cannot delete or update a parent row: a foreign key constraint fails。这不是语法问题,而是数据库的参照完整性保护机制在起作用。
- 不能靠“先删子表再删父表”硬编码顺序——批量删除时子表关联可能跨多层(比如 parent → child → grandchild)
- 不能依赖应用层逐条判断——性能差,且并发下容易出现中间状态不一致
- ON DELETE CASCADE 虽然能自动级联,但多数生产环境禁用,因为风险不可控(误删扩散)
用 WITH RECURSIVE 构建完整待删ID集合(MySQL 8.0+ / PostgreSQL)
核心思路:从输入的父ID列表出发,递归查出所有直接/间接子节点ID,一次性收集全量待删ID,再分表删除。这样避免了循环依赖和多次查询开销。
以 MySQL 为例,假设表结构为 categories(id, name, parent_id):
WITH RECURSIVE to_delete AS ( SELECT id FROM categories WHERE id IN (101, 102) UNION ALL SELECT c.id FROM categories c INNER JOIN to_delete td ON c.parent_id = td.id ) SELECT id FROM to_delete;
- 必须把递归结果存入临时表或 CTE,不能在 DELETE 中直接嵌套递归查询(MySQL 不支持)
- PostgreSQL 允许
DELETE FROM categories WHERE id IN (WITH RECURSIVE ...),但 MySQL 需拆成两步:先 INSERT INTO temp_ids SELECT ...,再 DELETE JOIN - 注意递归深度限制:
cte_max_recursion_depth默认 1000,超限时会报错ERROR 3636
分表按依赖顺序执行 DELETE(兼容老版本 MySQL)
如果数据库不支持递归 CTE(如 MySQL 5.7),就得手动控制删除顺序:先删最深层子表,再向上逐层删。关键不是“从上往下”,而是“从下往上”。
- 先确认外键依赖链:用
SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'parent_table' - 构造删除语句时,确保子表 DELETE 在父表之前执行;可用临时表暂存待删父ID,再分别 JOIN 删除
- 示例片段(安全起见加事务):
START TRANSACTION; CREATE TEMPORARY TABLE _to_delete_parent (id BIGINT PRIMARY KEY); INSERT INTO _to_delete_parent SELECT id FROM parent WHERE status = 'deleted'; DELETE c FROM child c INNER JOIN _to_delete_parent p ON c.parent_id = p.id; DELETE p FROM parent p INNER JOIN _to_delete_parent t ON p.id = t.id; DROP TEMPORARY TABLE _to_delete_parent; COMMIT;
- 临时表名带下划线前缀,避免与业务表冲突;不用 MEMORY 引擎,防止大结果集溢出
触发器或存储过程里如何避免死锁和超时
批量删除过程中,并发请求可能因锁等待导致 ERROR 1205 (40001): Deadlock found when trying to get lock 或 ERROR 1205。这不是代码写错,而是行锁升级和等待顺序问题。
- 统一按 ID 升序处理:对输入ID数组先排序,再逐个或分批处理,让各会话加锁顺序一致
- 避免在存储过程中调用函数做复杂计算——尤其是涉及 SELECT FOR UPDATE 的逻辑,容易延长锁持有时间
- 设置合理超时:
SET SESSION innodb_lock_wait_timeout = 30;(默认50秒,太高会拖慢整体响应) - 慎用
DELETE ... LIMIT分页删——自增ID不连续时容易漏删;改用WHERE id BETWEEN ? AND ?更可控
真正麻烦的从来不是“怎么删”,而是删的时候有没有人正在往子表插数据、有没有未提交事务占着父记录的锁。这些细节不显眼,但线上一出问题就是雪崩起点。











