子查询性能关键在于执行计划中的type和key:IN子查询外层字段需索引,EXISTS子查询内层关联字段需索引,标量子查询需联合索引覆盖,派生表需内部索引支持,务必通过EXPLAIN验证。

IN 子查询外层字段必须有索引,否则全表扫描
IN 类型子查询执行时,MySQL 先执行右边子查询得到结果集,再对外层表做哈希匹配。这个过程不依赖子查询是否走索引,而取决于外层 WHERE 条件字段有没有索引。
- 如果写成
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid'),重点不是orders.status是否有索引(它影响子查询本身快慢),而是users.id必须有索引,否则外层遍历就是type=ALL - 子查询内部若涉及大表(如
orders行数百万),它的WHERE条件字段(这里是status)也得有索引,否则子查询执行就慢,拖累整体 - 避免
NOT IN:只要子查询返回任意NULL,整行被过滤;改用NOT EXISTS,且同样要求关联字段索引
EXISTS 子查询依赖内层关联字段索引
EXISTS 通常被优化器转为半连接(semi-join),驱动顺序是外层表 → 内层表,所以性能瓶颈在内层表能否快速定位。
- 写法如
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) - 关键索引必须建在
orders(user_id)上,而不是users(id)(外层主键通常已有索引,但不是这里的关键) - 如果内层表还有额外条件,比如
o.status = 'shipped',复合索引orders(user_id, status)比单列user_id更稳 -
EXPLAIN看key列是否命中该索引,type是否为ref或range,而非ALL
标量子查询必须用联合索引覆盖定位 + 聚合
SELECT id, (SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS cnt FROM users u 这类写法每行都触发一次子查询,N+1 本质没变,索引设计稍偏就退化。
- 必须建联合索引
orders(user_id, order_id)(order_id占位即可,让COUNT(*)走索引覆盖,避免回表) - 如果
users.id不是主键或唯一键,外层扫描可能无法下推条件,导致子查询执行次数爆炸 - 更可靠的做法是提前聚合:
SELECT u.id, COALESCE(o.cnt, 0) FROM users u LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id) o ON o.user_id = u.id
派生表(FROM 中的子查询)默认无索引,不能直接加
把子查询当表用,比如 SELECT * FROM (SELECT user_id, SUM(amount) FROM orders GROUP BY user_id) t JOIN users u ON t.user_id = u.id,这个 t 是临时结果集,原表索引不继承,外部也无法给它建索引。
- 唯一能干预的方式,是在子查询内部尽量走索引:比如
orders的GROUP BY user_id要求user_id有索引,否则type=ALL+Using temporary; Using filesort - 复杂派生表建议物化为临时表或持久化视图,并在关键字段上手动建索引(仅限支持的引擎,如 MySQL 8.0+ 的不可见索引或通过 CREATE TEMPORARY TABLE 显式建)
-
EXPLAIN里看到select_type=DERIVED时,务必检查其key和rows,别默认它“用了索引”
真正决定子查询能不能用上索引的,从来不是“写了子查询”,而是你有没有盯住执行计划里的 type 和 key —— 它们不会说谎,但容易被忽略。

















