inner join 加表变慢主因是缺失索引导致嵌套循环,需为 join 条件字段单独建索引,结合 where/order by 字段创建复合索引,并用 explain 验证执行计划,避免隐式转换与驱动表误选。

为什么 INNER JOIN 一加表就变慢?
不是 JOIN 本身慢,是数据库在没索引时被迫做嵌套循环(Nested Loop)——比如 A 表 10 万行 × B 表 8 万行,最坏要算 80 亿次匹配。MySQL/PostgreSQL 都会这样,除非你告诉它“别瞎找,按这个字段快速定位”。
-
EXPLAIN 是必查的第一步,重点看 type 列:出现 ALL 或 index(全表扫描或全索引扫描)基本就是没走对路
- JOIN 条件字段必须单独建索引,
ON a.user_id = b.user_id 要求 a.user_id 和 b.user_id 各自都有索引,缺一个就可能退化
- 复合索引要注意顺序:
WHERE status = 'active' AND user_id = 123 + JOIN ... ON user_id,优先建 (user_id, status),不是反过来
哪些字段该加索引?别只盯 ON 子句
JOIN 性能瓶颈常藏在 WHERE、ORDER BY、GROUP BY 里。数据库优化器可能先过滤再关联,也可能先关联再过滤——取决于索引是否覆盖全部关键路径。
- 索引要“覆盖查询”:如果
SELECT name, email FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid' ORDER BY o.created_at DESC,理想索引是 orders(status, created_at, user_id)(含 JOIN 字段 + 过滤字段 + 排序字段)
-
user_id 在 orders 表上已有索引,但若同时要查 status 和排序 created_at,单列索引大概率被忽略
- 小心隐式类型转换:比如
user_id 是 BIGINT,但 JOIN 时写成字符串 '123',索引直接失效,EXPLAIN 里会显示 type: ALL
LEFT JOIN 比 INNER JOIN 还慢?多半是驱动表错了
优化器会选“小结果集”作为驱动表(outer table),但如果你的 LEFT JOIN 左表实际返回几十万行,右表只有几百行,却没强制指定,数据库可能反着来——用大表去循环小表,IO 和内存压力翻倍。
- 用
STRAIGHT_JOIN(MySQL)或 /<em>+ Leading(...) </em>/(PostgreSQL via pg_hint_plan)手动指定驱动表顺序
- 不要迷信“小表放左边”:真实行数得看执行计划里的
rows 列,不是看表总大小
- LEFT JOIN 的右表如果带 WHERE 条件(如
WHERE r.deleted = false),等价于把 LEFT 变成了 INNER,还失去索引下推机会——这种时候不如拆成两个查询或改用 EXISTS
临时表 or CTE 能提速?小心物化陷阱
有人看到多层嵌套 JOIN 就想用 WITH tmp AS (...) 先筛出中间结果,但 PostgreSQL 12+ 默认不物化 CTE,MySQL 根本不支持标准 CTE 物化,结果只是语法糖,该扫的表一次没少扫。
- MySQL 8.0+ 中
WITH RECURSIVE 才强制物化,普通 CTE 不保证;PostgreSQL 需加 MATERIALIZED 提示(v12+)
- 更稳妥的做法是建
CREATE TEMPORARY TABLE 并立刻在上面建索引,尤其当中间结果
- 临时表记得加
DROP TEMPORARY TABLE 或依赖连接自动清理,否则长连接下会堆积
EXPLAIN 是必查的第一步,重点看 type 列:出现 ALL 或 index(全表扫描或全索引扫描)基本就是没走对路 ON a.user_id = b.user_id 要求 a.user_id 和 b.user_id 各自都有索引,缺一个就可能退化 WHERE status = 'active' AND user_id = 123 + JOIN ... ON user_id,优先建 (user_id, status),不是反过来 - 索引要“覆盖查询”:如果
SELECT name, email FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'paid' ORDER BY o.created_at DESC,理想索引是orders(status, created_at, user_id)(含 JOIN 字段 + 过滤字段 + 排序字段) -
user_id在orders表上已有索引,但若同时要查status和排序created_at,单列索引大概率被忽略 - 小心隐式类型转换:比如
user_id是BIGINT,但 JOIN 时写成字符串'123',索引直接失效,EXPLAIN里会显示type: ALL
LEFT JOIN 比 INNER JOIN 还慢?多半是驱动表错了
优化器会选“小结果集”作为驱动表(outer table),但如果你的 LEFT JOIN 左表实际返回几十万行,右表只有几百行,却没强制指定,数据库可能反着来——用大表去循环小表,IO 和内存压力翻倍。
- 用
STRAIGHT_JOIN(MySQL)或 /<em>+ Leading(...) </em>/(PostgreSQL via pg_hint_plan)手动指定驱动表顺序
- 不要迷信“小表放左边”:真实行数得看执行计划里的
rows 列,不是看表总大小
- LEFT JOIN 的右表如果带 WHERE 条件(如
WHERE r.deleted = false),等价于把 LEFT 变成了 INNER,还失去索引下推机会——这种时候不如拆成两个查询或改用 EXISTS
临时表 or CTE 能提速?小心物化陷阱
有人看到多层嵌套 JOIN 就想用 WITH tmp AS (...) 先筛出中间结果,但 PostgreSQL 12+ 默认不物化 CTE,MySQL 根本不支持标准 CTE 物化,结果只是语法糖,该扫的表一次没少扫。
- MySQL 8.0+ 中
WITH RECURSIVE 才强制物化,普通 CTE 不保证;PostgreSQL 需加 MATERIALIZED 提示(v12+)
- 更稳妥的做法是建
CREATE TEMPORARY TABLE 并立刻在上面建索引,尤其当中间结果
- 临时表记得加
DROP TEMPORARY TABLE 或依赖连接自动清理,否则长连接下会堆积
STRAIGHT_JOIN(MySQL)或 /<em>+ Leading(...) </em>/(PostgreSQL via pg_hint_plan)手动指定驱动表顺序 rows 列,不是看表总大小 WHERE r.deleted = false),等价于把 LEFT 变成了 INNER,还失去索引下推机会——这种时候不如拆成两个查询或改用 EXISTS WITH tmp AS (...) 先筛出中间结果,但 PostgreSQL 12+ 默认不物化 CTE,MySQL 根本不支持标准 CTE 物化,结果只是语法糖,该扫的表一次没少扫。
- MySQL 8.0+ 中
WITH RECURSIVE才强制物化,普通 CTE 不保证;PostgreSQL 需加MATERIALIZED提示(v12+) - 更稳妥的做法是建
CREATE TEMPORARY TABLE并立刻在上面建索引,尤其当中间结果 - 临时表记得加
DROP TEMPORARY TABLE或依赖连接自动清理,否则长连接下会堆积
索引不是越多越好,但 JOIN 字段没索引,几乎一定慢;EXPLAIN 不看,等于闭眼调优;驱动表和过滤条件谁先谁后,实际执行计划说了才算。










