JOIN查询慢主因是索引设计不当:ON字段必须建索引,优先复合索引;驱动表确定后,被驱动表连接字段索引更重要;外键不自动建索引需手动添加;低区分度字段不宜单独建索引;函数操作会使索引失效。

JOIN 查询慢,八成是因为索引没建对——不是“有没有”,而是“建在哪、怎么建、谁驱动谁”。
JOIN 的 ON 字段必须有索引,且优先用复合索引
MySQL 在执行 JOIN ... ON a.x = b.y 时,会尝试在 a.x 或 b.y 上走索引。如果两边都没索引,就会触发全表扫描或 Block Nested-Loop(BNL),性能断崖下跌。
- 单列索引够用时就别硬上复合索引,比如
order_id是主键,ON o.order_id = oi.order_id中oi.order_id单独建INDEX(order_id)就行 - 但若同时过滤+连接,比如
WHERE o.create_time > '2023-01-01' AND o.customer_id = 123 JOIN ... ON o.order_id = oi.order_id,那orders表上更适合建INDEX(create_time, customer_id, order_id)—— 前导列必须是 WHERE 最左条件,末尾带上 JOIN 字段,才能覆盖过滤+连接+避免回表 - 注意字段顺序:复合索引中,等值条件字段放前,范围条件(如
BETWEEN、>)放后,否则后续字段索引失效
驱动表的连接字段索引比被驱动表更重要
MySQL 优化器通常选小结果集的表当驱动表。一旦确定了驱动表(看 EXPLAIN 第一行),它扫出来的每一行,都要去被驱动表查匹配数据——这时,被驱动表的连接字段**必须有索引**,否则就是嵌套循环全表扫。
- 例如
EXPLAIN显示table列先出现customers,再是orders,说明customers是驱动表,orders.customer_id就得有索引(哪怕customers.id是主键) - 反过来说,如果
orders被选为驱动表(比如加了WHERE orders.create_time BETWEEN ...且该字段有索引),那customers.id也得有索引,否则JOIN时查客户信息就慢 - 不要只盯着“外键字段”建索引——外键约束不等于索引,
ALTER TABLE orders ADD FOREIGN KEY (customer_id) REFERENCES customers(id)不会自动建索引,必须手动ADD INDEX idx_customer_id (customer_id)
多表 JOIN 时,每张被驱动表至少一个连接字段索引
三张表 A JOIN B ON a.id = b.a_id JOIN C ON b.id = c.b_id,B 是中间表,它既是 A 的被驱动表,又是 C 的驱动表。这种链式 JOIN 极易出问题。
- B 表上至少要有两个索引:
INDEX(a_id)(供 A 驱动时查找)、INDEX(id)(最好是主键,供驱动 C) - 如果 C 还要按
c.status = 'active'过滤,那就得考虑INDEX(b_id, status),让连接+过滤一步到位 - 避免在被驱动表的非索引字段上
ORDER BY或GROUP BY,否则会强制用临时表+文件排序,EXPLAIN里看到Using temporary; Using filesort就得警惕
索引不是越多越好,要注意选择性和维护成本
给每个 JOIN 字段都建索引,看起来稳妥,实则埋雷。
-
gender、status这类低区分度字段(比如只有 'M/F' 或 '0/1'),单独建索引几乎无效,优化器会直接跳过,改用全表扫描 - 写多读少的表,高频
INSERT/UPDATE会拖慢,因为每个索引都要更新;千万级订单表加 5 个索引,插入延迟可能翻倍 - 用
SELECT COUNT(*) FROM table WHERE col = 'x'粗略验证选择性:如果返回行数 > 总行数的 5%,这个字段就不适合做索引前导列
最常被忽略的一点:索引生效依赖查询写法。比如 ON DATE(o.create_time) = '2023-01-01' 会让 create_time 索引完全失效——函数操作导致无法走索引,得改成 o.create_time >= '2023-01-01' AND o.create_time < '2023-01-02'。


















