关联子查询必然导致N次内层执行,若内层无索引即N次全表扫描;其无法物化因依赖外层列,MySQL 5.7前几乎不优化,8.0遇复杂条件仍退化;JOIN改写通过一次聚合+索引连接规避线性恶化。

关联子查询必然导致全表扫描,不是“可能”,而是执行模型决定的——只要外层表有 N 行,内层就得跑 N 次,每次若没走索引,就是 N 次全表扫描。
为什么相关子查询无法提前物化结果
因为子查询里引用了外层表的列(比如 u.department = u2.department),数据库根本没法独立算出一个固定结果。它只能等外层每取出一行,才把该行的值代入子查询重跑一遍。MySQL 5.7 及更早版本几乎不做物化优化;8.0 虽支持部分物化,但碰到复杂条件(如带 ORDER BY、LIMIT 或非等值连接)仍会退回到逐行执行。
- EXPLAIN 中看到大量
DEPENDENT SUBQUERY就是明确信号 - PostgreSQL 的
LATERAL语义等价,但执行器仍倾向嵌套循环,统计信息不准时照样选顺序扫描 - 哪怕外层只返回 10 行,内层表若没在关联字段(如
u2.department)上建索引,就会扫 10 次全表
IN (SELECT ...) 在什么情况下等于主动放弃索引
不是所有 IN 都慢,但以下写法基本宣告索引失效:
-
WHERE id IN (SELECT user_id FROM users WHERE vip_level > 3):子查询无user_id索引,或vip_level是范围条件(>),MySQL 很难用index_merge,常选全表扫描 - 子查询里用了函数、隐式转换(如
WHERE phone = 13800000000,而phone是 VARCHAR)、IS NULL,都会让优化器弃用索引 - 子查询被物化成临时表后,外层
IN查这个临时表,type=ALL —— 这个临时表没索引,只能扫
JOIN 改写为什么能避开全表扫描
本质是把“为每行重复计算”变成“一次聚合 + 一次哈希/索引连接”。只要驱动表过滤条件能走索引,JOIN 就能利用索引下推和哈希聚合加速。
- 原写法:
SELECT * FROM orders o WHERE o.user_id IN (SELECT user_id FROM users WHERE vip_level > 3) - 改写后:
SELECT o.* FROM orders o JOIN users u ON o.user_id = u.user_id WHERE u.vip_level > 3 - 关键点:
users表加复合索引(vip_level, user_id),就能跳过全表扫描,直接定位匹配行 - 注意:JOIN 后如果还要去重或排序,得额外加
DISTINCT或GROUP BY,否则可能因一对多产生重复行
真正容易被忽略的是:关联子查询的性能恶化是线性的,10 行外层 → 10 次内层扫描;10 万行外层 → 10 万次内层扫描。而 JOIN 是常数级或对数级复杂度,差别不在写法“看起来简洁”,而在执行路径是否可复用索引和中间结果。

















