嵌套查询本身不加锁,死锁源于子查询执行时机导致加锁顺序失控;innodb可能提前物化子查询并加锁,与外层访问顺序冲突形成循环等待。

嵌套查询本身不加锁,但子查询执行时机让锁顺序失控
死锁不是因为写了 SELECT ... WHERE id IN (SELECT ...) 这种语法,而是子查询在事务中实际执行的时刻、扫描路径、加锁范围与外层语句不一致。InnoDB 对子查询可能提前物化并加锁,而主查询再按另一顺序访问同一行——比如事务A先锁了 t2.id=100(子查询结果),再去锁 t1.id=200(主表更新);事务B反向操作,就卡死了。
常见错误现象:Deadlock found when trying to get lock 报错,但 EXPLAIN 显示子查询走索引,仍死锁。原因往往是子查询虽走索引,但优化器选择“物化临时表”后逐行回表,导致加锁顺序脱离主键顺序控制。
- MySQL 5.7+ 默认启用
materialization,子查询结果会生成临时表,再关联主表——这期间可能对临时表行和主表行分批加锁,顺序不可控 - PostgreSQL 的
WITH RECURSIVE若在 UPDATE 中嵌套,递归展开过程若无唯一终止条件,可能反复尝试锁定同一行,形成隐式循环等待 - 别依赖“子查询简单就安全”——哪怕只是
SELECT id FROM t2 WHERE status = 'pending',如果status没索引,就会全表扫描 + 全表临键锁,把整个候选集都锁住
IN/EXISTS 嵌套最危险:两个事务对同一张表加锁方向相反
这是线上高频死锁来源。例如 UPDATE t1 SET x=1 WHERE id IN (SELECT t2.id FROM t2 WHERE t2.flag = 1),事务A可能按 t2 表聚簇索引顺序扫描加锁,再更新 t1;事务B却先锁了部分 t1 行,再查 t2 ——两边加锁路径交叉,极易成环。
实操建议:
- 用
JOIN重写:把语句改成UPDATE t1 JOIN t2 ON t1.id = t2.id SET t1.x = 1 WHERE t2.flag = 1,强制优化器走 Nested Loop,并统一以t2主键顺序驱动,锁顺序可预期 - 禁用物化(仅调试用):
SET optimizer_switch='materialization=off';强制走IN的半连接优化,减少中间结果锁 - 若必须保留子查询,先
SELECT id FROM t2 WHERE ... ORDER BY id拿到有序 ID 列表,再拼进UPDATE ... WHERE id IN (...),确保外层按主键升序加锁
ORDER BY 和 LIMIT 在子查询里是隐形炸弹
很多人想“先取 top 10 再更新”,于是写 UPDATE t1 SET x=1 WHERE id IN (SELECT id FROM t2 ORDER BY created_at DESC LIMIT 10)。问题在于:数据库很可能放弃索引,转而物化全部 t2 数据再排序——等于把整张 t2 表的候选行都锁住,且顺序随机。
性能与兼容性影响:
- MySQL 8.0+ 对含
LIMIT的子查询会自动禁用物化,但前提是外层没FOR UPDATE;一旦加了,仍可能升级为全表扫描锁 - PostgreSQL 中
ORDER BY ... LIMIT在子查询里会触发Sort节点,若数据量大,临时磁盘排序+锁行时间拉长,提高冲突概率 - 绝对不要在子查询里混用
ORDER BY和FOR UPDATE,因为排序字段若无索引,FOR UPDATE会直接锁住所有被扫描的行(包括未命中LIMIT的那些)
隔离级别和索引缺失会让嵌套查询锁范围指数级扩大
默认 REPEATABLE READ 下,嵌套查询中任何范围条件(如 BETWEEN、>=)都会触发间隙锁(Gap Lock),子查询扫一个区间,可能把整个索引段都锁死。而索引缺失时,连间隙锁都保不住——直接升级为表锁或大量行锁。
容易踩的坑:
-
WHERE id IN (SELECT user_id FROM logs WHERE ts > '2026-04-01'):如果logs(ts)没索引,子查询全表扫描,每行都加临键锁,锁住成千上万行 - 设了
READ COMMITTED就万事大吉?错。它虽取消间隙锁,但子查询物化行为可能变化,原本走索引嵌套循环的,现在改走 Hash Join,锁时机前移,照样冲突 - 只给主查询字段建索引没用——必须检查子查询
WHERE条件字段是否命中索引。用EXPLAIN看type是否为ref或range,不是ALL或index
真正难处理的,是那种看似只锁几行、但因索引失效或执行计划漂移,实际锁了整段索引的嵌套查询——它不会报错,但并发一上来就卡死,且日志里看不出明显异常。











