关联子查询比非关联子查询更易死锁,因其每行外层数据都触发一次子查询执行,导致“读+锁”嵌套循环,在UPDATE/DELETE中以不可控时机、顺序和行集加锁,显著提升循环等待概率。

关联子查询比非关联子查询更容易造成死锁,根本原因不是“它多执行了几次”,而是它迫使数据库在不可控的时机、以不可控的顺序、对不可控的行集加锁——尤其在 UPDATE / DELETE 场景下,这种不确定性直接放大了循环等待概率。
为什么关联子查询会触发多次、跨事务的锁行为
非关联子查询只执行一次,结果物化后供外层使用,锁也集中在物化阶段(如扫描 t2 时加一次间隙锁);而关联子查询每行外层数据都触发一次子查询执行,等于把“读+锁”操作嵌套进外层循环里:
- UPDATE orders SET status = 1 WHERE id IN (SELECT order_id FROM logs WHERE user_id = orders.user_id) → 对每个
orders行,都重新扫描logs并加锁 - 若两个事务分别处理
orders行 A 和 B,而它们匹配的logs行集合交叉(比如 A→log_100/log_200,B→log_200/log_300),就极易形成 A 持 log_100 等 log_200、B 持 log_200 等 log_300 的链式等待 - MySQL 5.7+ 默认启用
materialization,但关联子查询无法物化,只能走DEPENDENT SUBQUERY,每次执行都可能触发新一批行锁甚至间隙锁
WHERE 中的关联子查询如何悄悄锁住整张表
看似只查一行,实则因缺乏索引或条件模糊,导致子查询退化为全表扫描:
-
SELECT * FROM orders o1 WHERE o1.id = (SELECT id FROM orders o2 WHERE o2.user_id = o1.user_id ORDER BY created_at DESC LIMIT 1)—— MySQL 5.7 无法下推LIMIT,o2每次都全扫orders,并对所有匹配行加临键锁(next-key lock) - 哪怕
o1只有 10 行,子查询也可能对 10 万行加锁,且这些锁持续到整个外层语句结束才释放 - 如果另一事务正按主键顺序更新
orders,而本事务按user_id+ 全表扫描加锁,两者锁路径完全错开 → 死锁高发
IN/EXISTS 里漏写表前缀,怎么就变成关联子查询了
语法上没写错,但字段解析歧义让数据库“误判”为关联,进而开启重复执行模式:
-
WHERE salary > (SELECT AVG(salary) FROM emp WHERE dept_id = dept_id)—— 后面的dept_id没加表别名,MySQL 默认它来自外层,于是变成关联子查询 - 更隐蔽的是表别名冲突:
SELECT * FROM emp e WHERE e.salary > (SELECT MAX(e.salary) FROM emp e),两个e让解析器无法区分作用域,同样触发关联逻辑 - 这类写法常通过测试(结果对),但
EXPLAIN里能看到DEPENDENT SUBQUERY,且SHOW ENGINE INNODB STATUS的LATEST DETECTED DEADLOCK区块会显示多个事务在emp的不同索引上互相WAITING FOR
真正麻烦的不是报错,是它不报错、结果也对,但响应时间从 300ms 慢到 8s,且只在并发压测或高峰时段暴露——因为锁冲突需要两个事务恰好以相反路径访问同一组行,这种时序依赖很难被单测覆盖。

















