explain中rows暴增说明join导致中间结果集失控,需立即优化:检查rows、type、filtered三列,确保索引有效、条件下推至on、避免隐式转换与null陷阱。

为什么EXPLAIN里rows突然暴增就是JOIN出问题了
不是等SQL跑完才看慢不慢,而是写完第一版就该跑EXPLAIN FORMAT=TRADITIONAL(MySQL)或EXPLAIN (ANALYZE, BUFFERS)(PostgreSQL)。重点盯三处:rows列、type字段、filtered百分比。
rows如果从1万跳到500万,基本就是中间结果集失控;type出现ALL或index说明没走索引;filtered低于5%意味着95%以上行被后续条件干掉——这部分本该提前压进ON里。
- LEFT JOIN右边表没加
WHERE条件?把它挪进ON,比如把WHERE b.status = 'active'改成ON a.id = b.a_id AND b.status = 'active' - ON里用了函数?比如
UPPER(a.name) = UPPER(b.name),数据库无法下推索引,直接放弃SARGable,先全量组合再过滤 - 复合JOIN漏字段?
ON a.id = b.a_id AND a.dt = b.dt少写a.dt = b.dt,数据量可能翻倍甚至指数级放大
临时表不是缓存,是提前固化过滤逻辑
超过5张表的JOIN,硬拼一条SQL大概率崩。但拆成多个独立SELECT在应用层合并,反而更慢——各子查询重复扫主表、无法共享t1.id上下文、大字段拖慢传输。
真正有效的临时表,是用CREATE TEMPORARY TABLE SELECT ...一步到位,只保留后续JOIN和WHERE真正用到的字段,比如只取id、status、created_at,坚决不带TEXT或超长VARCHAR。
- 索引必须按实际JOIN顺序建:
JOIN temp_t3 ON t1.b = temp_t3.b,那INDEX(b)是底线;如果还有WHERE temp_t3.status = 'active',就建INDEX(b, status),别倒过来 - 主表(如
orders)一般不放临时表——它是驱动源,提前过滤反而丢失时机 - 被
LEFT JOIN的右表,如果本身只有几千行,没必要临时化;如果是百万级且只用其中某天的数据,就值得
JOIN字段没索引,等于裸奔
两张百万级表JOIN,没索引时MySQL默认用Nested Loop Join,复杂度O(M×N)。200万 × 100万 = 2万亿次比较,CPU直接满载。
有索引后变成B+树查找,单次匹配从O(N)降到O(logN),200万行对应约21次磁盘IO。验证很简单:EXPLAIN里type要是ref或eq_ref,key字段非空。
-
orders.user_id上必须建索引,哪怕它已是外键——外键约束不自动建索引 - 复合索引要覆盖
JOIN + WHERE:比如WHERE u.status = 'active' AND u.city = 'shanghai',那就建INDEX(status, city, id),id放最后用于回表 - 字段类型不一致会隐式转换:
t1.a是VARCHAR(20),t2.a是VARCHAR(50),JOIN时若没显式CAST,索引可能失效
LEFT JOIN的NULL陷阱比语法更危险
LEFT JOIN本身不报错,但右边表如果是一行对多行(比如聚合结果没加DISTINCT或GROUP BY),左表会被悄悄放大。更隐蔽的是:右边表数据分布不均时,比如99%的user_id只对应1条订单,但1%的VIP用户有上千条,优化器按平均值估算rows,实际执行却爆内存。
- 右表是子查询?确保它带
LIMIT或强过滤条件,否则MySQL 8.0之前默认不物化派生表,可能反复执行 - 右表有
OR条件?比如ON a.id = b.x OR a.id = b.y,优化器常放弃索引合并,退化为全表扫描 - 用
SEMI JOIN替代IN或EXISTS(部分数据库支持),能避免右表重复匹配同一行
复杂点不在语法有多难,而在中间结果集是否可控——所有优化动作,本质都是让rows数字从“猜”变成“控”。











