mysql子查询慢的主因是重复执行,5.6前in子查询易被展开为嵌套循环;应改用join、启用semijoin、物化临时表或窗口函数,并确保子查询自身有索引。

子查询被重复执行导致慢查询
MySQL 5.6 之前,IN 子查询常被优化器展开为嵌套循环,外层每行都触发一次子查询执行——哪怕子查询结果完全不变。比如 SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE region = 'CN'),若 customers 表有 10 万行匹配,orders 表有 50 万行,最坏可能执行 50 万次子查询。
- 优先改写为
JOIN:把子查询提前物化成临时表或直接关联,让优化器有机会用索引驱动 - 确认 MySQL 版本:5.7+ 默认启用
semijoin优化,但需确保optimizer_switch中semijoin=on - 用
EXPLAIN检查type列:若出现ALL或index且rows极大,大概率是子查询未被物化
相关子查询(correlated subquery)卡死在大表上
形如 SELECT o.*, (SELECT COUNT(*) FROM items i WHERE i.order_id = o.id) AS item_count FROM orders o 这类语句,子查询依赖外层字段,无法一次性物化,必须逐行求值。当 orders 表超 10 万行,性能断崖式下跌。
- 改用
LEFT JOIN + GROUP BY:先聚合再关联,避免逐行调用 - 检查
items.order_id是否有索引:没有的话,每次子查询都是全表扫描 - 若必须保留子查询结构(如 ORM 限制),加
LIMIT 1或EXISTS替代SELECT ... FROM——只要判断存在性,别算总数
子查询返回多列或多行引发报错或隐式截断
WHERE col = (SELECT a, b FROM t) 直接报错 Subquery returns more than 1 row;而 WHERE col IN (SELECT a FROM t) 看似安全,但如果 a 是 TEXT 或长字符串,MySQL 可能因排序缓冲区不足退化为文件排序,拖慢整个查询。
- 严格匹配单值场景用
= (SELECT ...)前,加LIMIT 1并确保业务逻辑允许取任意一行 -
IN子查询中避免SELECT *或无谓的多列输出,只选真正需要的字段 - 若子查询结果集预期 > 1000 行,考虑改用临时表 + 索引:先
CREATE TEMPORARY TABLE tmp_ids AS SELECT id FROM ...,再JOIN tmp_ids
窗口函数替代子查询却忽略执行计划变化
MySQL 8.0+ 支持 ROW_NUMBER()、RANK() 等,容易误以为“用了新语法就一定快”。但窗口函数在分区大、排序字段无索引时,仍会触发临时表和 filesort,比等价的 JOIN 更耗内存。
- 用
EXPLAIN FORMAT=TREE查看是否出现Using temporary; Using filesort - 窗口函数的
PARTITION BY字段必须有索引,否则分区数据无法局部排序 - 简单排名/累计需求(如“每个用户最新一笔订单”),
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)+ 外层WHERE rn = 1通常比自连接子查询清晰,但务必验证created_at上有索引
实际调优时,最常被跳过的一步是确认子查询本身的执行效率——先单独跑一遍子查询,看它本身是否需要加索引或重写。很多“子查询慢”问题,根子其实在子查询内部没走索引,而不是外层写法不对。











