根本原因是相关子查询被迫逐行重算且内层无索引,导致外层每行都触发全表扫描;应改用EXISTS或JOIN并确保关联字段有索引。

EXISTS 和 IN 子查询在千万级数据下极易触发全表扫描,根本原因不是语法本身有问题,而是执行路径失控——数据库被迫为外层每一行重复执行内层扫描,且无法跳过无用数据。
相关子查询必然逐行重算,无法物化
只要子查询里引用了外层表的列(比如 u.department 或 o.customer_id),它就是相关子查询。数据库不能提前算出结果存起来,必须等外层某一行进来后,代入新值再跑一遍内层逻辑。
- 外层返回 10 万行 → 内层最多执行 10 万次
- 若内层没索引,每次都是
type=ALL扫全表 → 总 I/O 是单次扫描的 10 万倍 - MySQL 5.7 及更早版本几乎不优化这类结构;8.0 虽支持部分物化,但只对简单聚合生效,且严重依赖统计信息准确度
IN 子查询在 MySQL 中可能退化为嵌套循环
看似简洁的 WHERE id IN (SELECT user_id FROM users WHERE status = 1),在数据量大、子查询未走索引时,MySQL 常选择“物化 + 循环匹配”策略:先建临时表存子查询结果,再对外表每行做哈希查找——但如果临时表过大或内存不足,就会降级为磁盘临时表 + 全表扫描驱动。
- 执行计划中出现
Using temporary和Using filesort是危险信号 - 当子查询返回行数超过几千,且外层表也很大时,
IN的实际代价常高于JOIN -
OR EXISTS这类组合结构更危险:执行计划里表现为多个FILTER节点嵌套,最小 ID 的子路径若返回大量行,会反复驱动更大 ID 的子路径执行数千次
索引失效让子查询彻底失去约束能力
即使写了子查询,如果内层 WHERE 条件没命中索引,等于白写。常见坑点包括:
- 联合索引最左前缀没对齐:比如建了
INDEX (status, created_at),但子查询写成WHERE created_at > '2023-01-01'→key_len显示只用了 0 字节,实际仍是全表扫 - 对字段用函数或表达式:
WHERE YEAR(created_at) = 2023→ 索引失效,只能扫全表再计算 - 子查询里用了
!=、OR、LIKE '%abc'等明确触发全表扫描的操作符
一眼识别子查询正在拖垮性能
别等超时报警。直接看 EXPLAIN FORMAT=JSON 输出里的三个硬指标:
-
type列为ALL或index,且对应行的Extra含Using where - 出现
DEPENDENT SUBQUERY,同时该行rows值远大于外层预估行数(比如外层预估 100 行,子查询却显示要扫 50 万行) -
dependent_contexts字段非空 → 明确表明这是相关子查询,且执行器已判定无法优化
真正难处理的从来不是语法怎么写,而是你得先确认:那个子查询到底有没有被当成独立单元执行?还是已经被优化器悄悄拆解成嵌套循环了?这一步错,后面所有索引、改写都白搭。

















