JOIN越多越慢主因是驱动表选错或临时表膨胀,子查询快因其主动筛选并利于索引使用,但相关子查询可能重复执行;改写须严守语义,避免数据丢失。

为什么 JOIN 越多越慢,而子查询有时反而快?
不是子查询天生快,而是 MySQL 在某些关联场景下会错误选择驱动表或生成大量临时结果集。EXPLAIN 里如果看到 type=ALL 或 rows 高得离谱,基本就是驱动表选错了。尤其当大表被当作被驱动表(即放在 JOIN 右侧但没走索引),性能会断崖式下跌。
子查询改写本质是把“被动等待关联”变成“主动筛选主表”,让优化器更大概率用上主表的索引。但注意:相关子查询(含 WHERE t1.id = t2.id 这种)仍可能被重复执行,不一定更快。
- 优先改写成「非相关子查询」,例如用
(SELECT GROUP_CONCAT(id) FROM t2 WHERE status=1)预计算结果再IN - 若必须用相关子查询,确保子查询里有能命中索引的条件,且字段类型与外层完全一致(比如都是
BIGINT,别一边INT一边VARCHAR) - 避免在子查询里用
ORDER BY + LIMIT配合IN——MySQL 5.7 及以前版本可能不走索引
EXPLAIN 看到 Using temporary; Using filesort 怎么办?
这俩提示说明 MySQL 正在内存或磁盘上建临时表排序,通常是 GROUP BY、DISTINCT 或多表 JOIN 后的 ORDER BY 引起的。根本原因常是排序字段没被索引覆盖,或者索引无法按查询顺序生效。
不要一上来就加 FORCE INDEX,先确认是否真需要排序结果。很多业务其实只取前几条,那就在子查询里提前 LIMIT,而不是等所有关联完再排。
- 联合索引要匹配
ORDER BY字段顺序,例如ORDER BY a, b需要(a,b)索引,(b,a)无效 -
GROUP BY字段必须和索引最左前缀一致,且不能混用 ASC/DESC(MySQL 8.0+ 支持,但老版本不行) - 如果
SELECT里有聚合函数又想避免临时表,考虑用WHERE过滤掉大部分数据后再GROUP BY,而不是先JOIN再过滤
哪些字段必须加索引,哪些加了也白加?
关联字段(ON 条件里的列)和 WHERE 过滤字段是刚性需求;但索引效果取决于区分度和查询模式。低区分度字段(比如 status TINYINT 只有 0/1)单独建索引几乎没用,除非配合其他高区分度字段组成联合索引。
容易被忽略的是:字符串字段做关联时,如果定义长度远超实际值(比如 VARCHAR(255) 存 6 位订单号),会导致索引效率下降,甚至让优化器放弃使用。
-
JOIN字段类型必须严格一致,INT和CHAR关联会触发隐式转换,索引失效 - 日期范围查询慎用
DATE(time_field),它会让time_field上的索引失效,改用time_field >= '2024-01-01' AND time_field - 复合索引中,等值查询字段放前面,范围查询字段放后面,例如
WHERE a=1 AND b>10 ORDER BY c,适合建(a,b,c)索引
改写后查询变快了,但数据不准了怎么办?
子查询改写最容易出错的地方是语义变化:原 LEFT JOIN 返回 NULL 行,改成 IN 子查询就直接过滤掉了;原 INNER JOIN 多对一产生重复行,改成子查询聚合又可能少数据。
这不是优化问题,是逻辑 bug。上线前必须用小数据集人工核对关键字段的行数、NULL 值比例、聚合结果,不能只比“执行时间”。
-
LEFT JOIN改EXISTS是安全的,改IN很可能丢数据 - 涉及
COUNT(*)或SUM()的,先用SELECT *对比原始JOIN和改写后的中间结果集差异 - 如果子查询用了
GROUP_CONCAT或JSON_ARRAYAGG,注意长度限制(group_concat_max_len默认 1024),超长会被截断
关联查询优化真正难的从来不是语法改写,而是判断哪一层过滤该前置、哪个 NULL 语义不能丢、以及索引字段类型和长度是否悄悄破坏了执行计划——这些地方一错,快是假快,准才是命门。











