相关子查询必然逐行重算,因其依赖外层列(如u.id、o.customer_id),数据库无法预计算缓存,必须为外层每行代入新值执行一次;外层10万行即触发10万次内层执行,若无索引则每次均为全表扫描。

相关子查询为什么必然逐行重算
只要子查询里引用了外层表的列(比如 u.id、o.customer_id),它就是相关子查询。数据库无法提前执行并缓存结果,必须等外层某一行进来后,把该行的值代入子查询再跑一次。这意味着:外层返回 10 万行 → 子查询最多被执行 10 万次。
MySQL 5.7 及更早版本几乎不优化这类结构;8.0 虽支持部分物化,但只对简单聚合生效,且严重依赖统计信息准确度。一旦统计不准或子查询含函数、OR、!= 等,物化就会失败,退回到逐行执行。
内层没索引 = 每次都是全表扫
相关子查询本身不等于全表扫描,但内层查询若没走索引,后果就是灾难性的:每次调用都触发一次 type=ALL 扫描。10 万次 × 单次 50 万行 = 总 I/O 是单次的 10 万倍。
常见导致内层失效的原因包括:
-
user_id是VARCHAR,但外层传的是数字123→ 触发CAST(user_id AS SIGNED),索引失效 - 联合索引是
(status, created_at),子查询却只写WHERE created_at > '2023-01-01'→key_len显示为 0,实际全表扫 - 子查询里用了
WHERE YEAR(created_at) = 2023或UPPER(name) = 'ALICE'→ 函数直接废掉索引
EXISTS 和 IN 在相关场景下同样危险
EXISTS 和 IN 看似语义不同,但在相关子查询中,它们的执行路径高度一致:都可能被优化器转成嵌套循环驱动。一旦内层没索引,EXPLAIN 里就会出现大量 DEPENDENT SUBQUERY 行,且每行的 rows 值巨大。
特别注意:OR 条件在 EXISTS 子句中危害极大。例如 WHERE EXISTS (SELECT 1 FROM orders WHERE user_id = u.id AND (status = 1 OR amount > 100)),只要 status 区分度低(如只有 0/1),优化器大概率放弃索引,直接全表扫描驱动。
怎么快速验证是不是相关子查询拖垮了性能
别等接口超时。直接看 EXPLAIN FORMAT=JSON 输出里的三个硬指标:
-
type列为ALL或index,且对应行的Extra含Using where - 出现
DEPENDENT SUBQUERY,同时该行rows远大于外层预估行数(比如外层预估 100 行,子查询却显示要扫 50 万行) -
dependent_contexts字段非空 → 明确表明这是相关子查询,且执行器已判定无法优化
真正难处理的从来不是语法怎么写,而是你得先确认:那个子查询,是不是正拿着一张没索引的表,在一遍遍地扫全表。

















