MySQL对5张以上表JOIN基本放弃优化,退化为嵌套循环,导致CPU满载、内存爆掉;临时表需窄字段、建索引、提前过滤,并显式DROP。

为什么5张表JOIN直接写一条SQL大概率崩
MySQL对超过5表的JOIN基本放弃优化,会退化为嵌套循环(Nested Loop),复杂度从O(N)变成O(M×N×...), 两百万行 × 百万行 × 十万行 = 数万亿次比较。CPU满载、内存爆掉、查询卡死是常态,不是“慢”,是执行路径失控。
常见错误现象:EXPLAIN里rows列从几千跳到几千万,type全是ALL或index,filtered长期低于5%——说明中间结果集爆炸,WHERE条件全在最后过滤,没下推。
- 别信“我数据量小所以没问题”:测试库10万行不卡,生产200万行就超时,因为优化器选错驱动表后,性能断崖式下跌
- LEFT JOIN越多越危险:每多一个LEFT JOIN,左表所有行都必须保留,右表无匹配也得补NULL,IO和内存开销翻倍
- 外键≠索引:哪怕
orders.user_id是外键,也必须手动建INDEX(user_id),否则JOIN字段裸奔
临时表怎么建才不拖慢反而加速
临时表不是“把数据先捞出来放着”,而是有明确目的的物理剪枝:只存后续真要用的字段、带索引、过滤条件提前下压。
错误写法:CREATE TEMPORARY TABLE tmp_orders AS SELECT * FROM orders——字段宽、没索引、没过滤,后续JOIN比原表还慢。
- 字段越窄越好:只选
id、user_id、status这类JOIN或WHERE用到的列,砍掉description、content等大字段 - 建表时显式声明主键或索引:用
CREATE TEMPORARY TABLE tmp_orders (id BIGINT PRIMARY KEY, user_id BIGINT, status VARCHAR(20)),别依赖AS SELECT自动推导 - 过滤必须在INSERT阶段完成:比如只取近30天已支付订单,就写
INSERT INTO tmp_orders SELECT id, user_id, status FROM orders WHERE status = 'paid' AND created_at >= '2026-08-05' - 建完立刻加索引:如果下一步要
JOIN userinfo ON tmp_orders.user_id = userinfo.id,马上执行CREATE INDEX idx_tmp_uid ON tmp_orders(user_id)
拆分JOIN时如何保持LEFT JOIN语义不丢NULL
LEFT JOIN的核心是“保左”,但临时表一建就容易只存“有匹配”的右表数据,导致原本该填NULL的行消失。
正确做法不是把右表单独建临时表,而是把整个LEFT JOIN逻辑前置固化:
- 若原SQL是
SELECT u.id, u.name, i.addr FROM user u LEFT JOIN userinfo i ON u.id = i.uid,先建带完整左表的临时表:CREATE TEMPORARY TABLE tmp_user AS SELECT id, name FROM user - 再LEFT JOIN右表生成中间结果:
CREATE TEMPORARY TABLE tmp_user_with_addr AS SELECT u.id, u.name, i.addr FROM tmp_user u LEFT JOIN userinfo i ON u.id = i.uid - 后续和其他表JOIN时,一律以
tmp_user_with_addr为起点,不再碰原始user表 - 千万别只建
tmp_userinfo然后LEFT JOIN tmp_userinfo——它没有左表上下文,优化器无法保证NULL填充
UPDATE场景下临时表JOIN比子查询快在哪
原生UPDATE t1 SET x = (SELECT y FROM t2 WHERE t2.id = t1.ref_id)是逐行执行子查询,EXPLAIN显示DEPENDENT SUBQUERY,每更新一行都重扫t2全表。
用临时表后变成批量哈希连接或索引查找,一次定位全部目标行:
- 临时表必须对关联字段建索引:如
tmp_updates含ref_id,就得CREATE INDEX idx_ref ON tmp_updates(ref_id) - UPDATE必须用INNER JOIN:写成
UPDATE t1 INNER JOIN tmp_updates t ON t1.id = t.ref_id SET t1.x = t.y;用LEFT JOIN会导致没匹配的行被设为NULL - 目标表对应字段也要有索引:
t1.id上没索引,UPDATE仍会全表扫描 - 别在ON里用函数:
ON UPPER(t1.name) = UPPER(t.name)会让索引失效,必须确保两边字段类型一致且无隐式转换
最易被忽略的一点:临时表生命周期绑定会话,不是事务。长事务中建了不DROP,可能被后续同名表覆盖或引发元数据冲突——每次用完显式DROP TEMPORARY TABLE tmp_xxx,别依赖自动销毁。

















