EXPLAIN可快速定位JOIN中拖慢性能的表,关键看type(ALL/index危险)、rows(预估扫描行数过大即瓶颈)、Extra(Using temporary/filesort需优先处理)。

EXPLAIN 一查就知哪张表在拖慢整个 JOIN
10 张表连在一起,性能瓶颈往往不在“表多”,而在某张表被全表扫描或驱动顺序错乱。先跑 EXPLAIN,重点盯三列:type(出现 ALL 或 index 就危险)、rows(某张表预估扫描行数上百万?它大概率是罪魁)、Extra(含 Using temporary 或 Using filesort 的表要优先处理)。
常见误区是直接改 SQL 结构,但真正该动的可能是索引——比如 orders.user_id 没索引,导致每次 JOIN users 都扫全表,10 张表里只要有一张这样,整体就卡死。
- 对每个
ON条件中的字段单独建索引,不要指望复合索引“顺便覆盖” -
LEFT JOIN右表的关联字段必须有索引,否则等于放弃优化 - WHERE 中的过滤条件,尽量下推到子查询或临时表中,别留在最外层
用临时表缓存中间结果比拆成多个 SELECT 更可靠
有人倾向把 10 表 JOIN 拆成 5 条独立 SELECT,再在应用层合并。这在数据量小、主键能对齐时可行,但真实场景常出问题:各子查询无法共享 t1.id 上的过滤结果,导致每条都重扫 t1;更糟的是,如果某张表只用到 id 的行,拆开后每条查询仍可能拉全表。
临时表能固化“已过滤 + 已索引”的中间状态:
- 先建
CREATE TEMPORARY TABLE temp_orders AS SELECT id, user_id, product_id FROM orders WHERE status = 'PAID' AND create_time > '2026-01-01'; - 立刻给
temp_orders加索引:ALTER TABLE temp_orders ADD INDEX idx_user_id (user_id); - 后续所有 JOIN 都基于这个几百行的小表跑,执行计划稳定,MySQL 也更容易选它当驱动表
注意:临时表生命周期只在当前会话,不用手动删;但别忘了加索引——没索引的临时表和原表一样慢。
驱动表选错会让 10 表 JOIN 变成指数级消耗
MySQL 默认用 Nested Loop Join:以第一张表为驱动表,逐行去匹配后面每张表。如果驱动表有 10 万行,而第二张表没索引,那光这张表就要扫 10 万次 × 全表行数。10 张表下来,不是慢,是不可控。
正确做法是让“筛选后最小”的表当驱动表:
- 别看物理大小,看
EXPLAIN里的rows值——哪个表经过 WHERE/子查询过滤后剩得最少,就把它放 JOIN 链最前面 - 避免用大表 LEFT JOIN 小表,因为 LEFT JOIN 会保留左表全部行,强制它当驱动表
- 如果业务允许,把高频过滤条件(如时间范围、状态码)提前塞进子查询,例如:
(SELECT * FROM logs WHERE dt = '2026-05-22') l,再让它 JOIN 其他表
拆分查询只在特定条件下真有效
拆成多条 SQL 不是银弹,它只在以下情况成立:
- 主表返回结果集很小(比如 IN 列表(注意 MySQL
max_allowed_packet限制) - 各从表之间无关联逻辑,纯属“主表 ID → 查 A 表字段、再查 B 表字段”这种线性关系
- 应用层能容忍轻微的数据不一致(比如两次查询之间主表数据被更新)
反例:你拆出 SELECT id FROM orders WHERE ...,再拿这些 ID 去查 users 和 products,但如果 orders 里某个 user_id 在 users 中已被删,LEFT JOIN 本该返回 NULL,而拆分后你查不到对应用户,就漏了这一行——这种语义差异容易被忽略,却很难回溯。
真正难的不是“怎么拆”,而是判断“该不该拆”。临时表方案更贴近数据库原生能力,语义保真度高,调试路径清晰;而拆分方案省的是数据库 CPU,换来的却是应用层逻辑膨胀和一致性风险。

















