MySQL 5.7及更早版本中IN(SELECT)未使用外层索引,因优化器将其改写为嵌套循环,外层全表扫描(EXPLAIN显示type=ALL、key=NULL);子查询是否走索引不影响该行为,而函数、类型转换等会使索引失效。

MySQL 5.7 及更早版本中 IN(SELECT) 直接绕过外层索引
不是“没走”,而是根本没尝试走——优化器把 WHERE id IN (SELECT user_id FROM logs WHERE status = 1) 改写成逐行执行的关联逻辑,先扫外层表每一行,再拿 id 去子查询里查一遍。外层 id 上哪怕有索引,也完全不会被用到。
典型表现是 EXPLAIN 中外层表的 type 为 ALL,key 为 NULL,rows 等于全表行数。
- 子查询本身是否走索引无关紧要,它只影响子查询内部性能,不改变外层全表扫描的事实
- 哪怕子查询加了
LIMIT 1,只要没被物化(materialize),老版本仍按嵌套循环执行 - MySQL 8.0+ 默认开启物化,但仅对简单子查询生效;含
GROUP BY、DISTINCT或相关子查询时,仍可能退化
子查询里用了函数或类型转换,索引直接失效
这不是子查询特有的问题,但常和子查询叠加出现,掩盖真正瓶颈。比如 WHERE id IN (SELECT user_id FROM logs WHERE DATE(created_at) = '2024-01-01'),DATE() 让 created_at 索引彻底失效。
同样,如果 user_id 是 VARCHAR 类型,而子查询写成 WHERE user_id = 123(数字),MySQL 会隐式转成字符串再比对,破坏索引有序性。
- 检查子查询的
EXPLAIN:若key为NULL,说明子查询自己就没走索引 - 避免在子查询
WHERE条件中对索引列用函数、表达式或强制类型转换 - 日期范围优先用
BETWEEN或>= AND ,而不是 <code>DATE()、YEAR()
IN 改 JOIN 时容易忽略的语义陷阱
把 IN 换成 JOIN 是最可靠解法,但不能只改语法。原 IN 的行为和 JOIN 天然不同:
-
IN对NULL敏感:若子查询返回NULL,整个条件变为UNKNOWN,该行被过滤;INNER JOIN则天然排除NULL,结果一致 - 但若子查询字段允许
NULL且业务逻辑需保留外层NULL匹配,就得补OR t.id IS NULL或改用LEFT JOIN - 子查询含
GROUP BY或聚合时,直接JOIN会放大主表行数(1 行变 N 行),必须先压平:(SELECT DISTINCT user_id FROM logs WHERE status = 1)
EXISTS 不等于万能,相关子查询仍可能慢
EXISTS 通常能保住外层索引,因为优化器可下推关联条件,但它不是性能银弹。
如果是相关子查询(如 WHERE EXISTS (SELECT 1 FROM logs WHERE logs.user_id = users.id AND logs.status = 1)),数据库仍可能对外层每行都执行一次子查询——索引还在,但 I/O 次数翻倍。
- 用
EXPLAIN看子查询是否标记为DEPENDENT SUBQUERY:是则说明逐行执行 -
NOT IN和NOT EXISTS行为差异极大:NOT IN遇到子查询任意NULL就整体返回空,NOT EXISTS完全不受影响 - PostgreSQL 用户可考虑
tencentdb_sublink插件,把部分EXISTS/IN自动上拉为JOIN,但 MySQL 无等效机制
EXPLAIN 输出里外层表的 key 和 rows 才是关键信号。一个看似简单的 IN,背后可能是执行计划层级的结构性绕过,不是加个索引就能解决的。

















