子查询删除本身不直接导致死锁,真正原因是多个事务对同一组行以不同顺序加锁;子查询的索引缺失、无序执行、物化机制及跨表依赖会放大加锁顺序不确定性,从而暴露并触发ab-ba循环等待。

子查询删除(DELETE ... WHERE id IN (SELECT ...))本身不直接导致死锁,真正触发死锁的是多个事务在并发下对**同一组行以不同顺序加锁**,而子查询的执行计划、索引缺失或优化器行为会放大这种不确定性。
子查询删除为什么容易暴露加锁顺序问题
MySQL 的 DELETE 语句在执行子查询时,会为子查询结果集中的每一行加锁(通常是临键锁 next-key lock),但加锁顺序取决于子查询是否走索引、是否排序、以及优化器选择的执行路径:
- 若子查询
WHERE条件没命中索引(如SELECT id FROM logs WHERE status = 'pending',而status无索引),MySQL 会全表扫描并逐行加锁——顺序由存储物理位置或主键隐式顺序决定,不可控 - 若两个事务分别执行类似语句:
DELETE FROM orders WHERE user_id IN (SELECT user_id FROM users WHERE region = 'CN')和DELETE FROM users WHERE id IN (SELECT user_id FROM orders WHERE created_at > ?),它们可能交叉锁定users和orders表的相同主键值,但顺序相反 - MySQL 5.7+ 默认启用
materialization(物化子查询),可能先建临时表再关联,期间锁行为与直连嵌套循环不同,进一步增加不可预测性
如何确认是子查询删除引发的死锁
执行 SHOW ENGINE INNODB STATUS\G 后,在 LATEST DETECTED DEADLOCK 段中重点看:
- 两个事务的 SQL 是否都含
IN (SELECT ...)结构,且涉及相同表(哪怕一张是主表、一张在子查询里) -
HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED中的锁是否分散在不同索引上(例如一个锁PRIMARY,另一个锁idx_region) - 事务堆栈里是否出现类似
TABLE LOCK table `db`.`logs` trx id ... lock mode IX+RECORD LOCKS space id ... page no ... n bits ...多层锁记录,说明子查询已提前锁住中间结果
避免子查询删除死锁的实操手段
核心不是禁用子查询,而是让锁行为可预期:
- 确保子查询
WHERE条件字段有**高效索引**:用EXPLAIN验证type是ref或const,而非ALL或index - 强制统一加锁顺序:把子查询拆成两步,先
SELECT id FROM ... ORDER BY id拿到有序主键列表,再用DELETE FROM t WHERE id IN (?, ?, ?)—— 这样所有事务都按id升序加锁 - 在 MySQL 中临时禁用物化(仅调试用):
SET optimizer_switch='materialization=off';,迫使优化器走DEPENDENT SUBQUERY,有时能缩小锁范围 - 避免在子查询中 JOIN 多表或使用
GROUP BY:这类操作易触发中间结果集加锁,且锁对象可能超出主表范围
最容易被忽略的一点:你以为在删 orders 表,但子查询里的 users 表可能已被其他事务长时间持有锁;死锁链往往藏在“被依赖的那张表”里,而不是你正在写的那条 DELETE 语句本身。










