子查询本身不直接造成锁等待,真正触发长时间锁的是MySQL(尤其5.7及之前)对子查询的物化行为和执行计划选择——它可能扫描远超预期的行数、加大量行锁或间隙锁,并把锁一直持到整个外层语句结束;EXPLAIN出现DEPENDENT SUBQUERY表示每行主表都重执行子查询,导致锁叠加;JOIN替代需规避非唯一键连接、NULL逻辑错误及聚合未收敛等陷阱;含窗口函数、ORDER BY LIMIT且外层需JOIN、跨分片等场景应拆至应用层;索引有效还需确保谓词命中最左前缀、无隐式转换;事务未及时提交才是最隐蔽的锁延长根源。

子查询本身不直接“造成”锁等待,真正触发长时间锁的是 MySQL(尤其 5.7 及之前)对子查询的物化行为和执行计划选择——它可能扫描远超预期的行数、加大量行锁或间隙锁,并把锁一直持到整个外层语句结束。
为什么 EXPLAIN 看见 DEPENDENT SUBQUERY 就要立刻警惕
这表示每处理一行主表数据,MySQL 都会重新执行一次子查询。哪怕主表只更新 10 行,子查询也可能被调用 10 次,每次扫全表或大范围索引区间,锁也随之叠加。
- 典型场景:
UPDATE t1 SET status=1 WHERE id IN (SELECT user_id FROM logs WHERE event='login' AND created_at > NOW() - INTERVAL 1 DAY),如果logs.event没索引,每次子查询都全表扫 -
EXPLAIN FORMAT=TRADITIONAL中select_type列出现DEPENDENT SUBQUERY,且rows值很大,基本等于锁风险等级 - MySQL 8.0+ 对某些含
ORDER BY ... LIMIT的子查询无法物化,也会退回到依赖型执行,不能仅凭版本放松检查
JOIN 替代 IN/EXISTS 时最容易踩的三个坑
JOIN 不是银弹,写错反而扩大锁范围或引发逻辑错误。
- 连接字段不是唯一键:
UPDATE t1 JOIN t2 ON t1.id = t2.ref_id SET t1.flag = 1,若t2.ref_id允许重复,一条t1记录可能被更新多次,且锁住所有匹配的t2行 - 漏掉 NULL 安全判断:
NOT IN (SELECT id FROM t2)改成LEFT JOIN t2 ON t1.id = t2.id WHERE t2.id IS NULL时,若t2.id允许为NULL,必须补上AND t2.id IS NOT NULL,否则NULL导致整个条件失效 - 聚合子查询没提前收敛:
SELECT * FROM orders WHERE user_id IN (SELECT user_id FROM events GROUP BY user_id HAVING COUNT(*) > 5),直接 JOIN 会放大主表行数;应先用 CTE 或派生表算出结果集:(SELECT user_id FROM events GROUP BY user_id HAVING COUNT(*) > 5) AS top_users
哪些子查询必须拆到应用层,别硬扛
当优化器已经无法安全下推过滤条件,或者执行计划里出现明显危险信号,继续压在 SQL 层只会让锁更隐蔽、更难定位。
-
EXPLAIN显示Using temporary; Using filesort,且rows远大于实际返回行数(比如扫 10 万行只返回 1 行) - 子查询含窗口函数(如
ROW_NUMBER() OVER (PARTITION BY x ORDER BY y))、GROUP_CONCAT、自定义函数,MySQL 无法将其下推到索引扫描阶段 - 子查询有
ORDER BY ... LIMIT 1取最新记录,但外层还要 JOIN 或聚合——此时即使有索引,排序也无法复用,大概率全表扫加锁 - 跨库或跨分片查询(如订单在分库、用户在主库),MySQL 根本无法协调分布式锁,事务边界天然模糊
索引有效 ≠ 子查询就安全
给子查询字段加了索引,但锁还是等很久?大概率是谓词没命中索引最左前缀,或存在隐式转换/函数调用。
-
WHERE DATE(create_time) = '2024-01-01'会让create_time索引完全失效;改成WHERE create_time >= '2024-01-01' AND create_time - 联合索引
(status, updated_at),写WHERE updated_at > '2024-01-01'用不上;必须带上status才能走索引 - 字段类型不一致:子查询里
user_id是VARCHAR,主表关联字段是INT,MySQL 会隐式转换,导致索引失效 - 字符集不同(如
utf8mb4vsutf8)也会触发隐式转换,查INFORMATION_SCHEMA.COLUMNS确认一致性
最易被忽略的点:事务生命周期比 SQL 本身更关键。一个子查询锁住 100 行,但如果事务没提交,这 100 行锁就挂在那里,不管后续有没有其他操作。锁等待的本质,常常不是子查询写得有多差,而是它被塞进了一个不该存在的长事务里。

















