存储过程JOIN变慢主因是中间结果集失控,而非语法错误;关键在关联字段洁净度、索引有效性及条件位置,需避免NULL干扰、ON中用函数、低效LEFT JOIN+WHERE,优先用EXISTS、先聚合再JOIN,并按最左前缀原则建复合索引。

存储过程里一加多表 JOIN 就慢,大概率不是语法写错了,而是中间结果集失控了——先连出几十万行,再过滤,CPU和内存全耗在搬运垃圾数据上。
为什么加了WHERE还是笛卡尔积?
根本不在有没有 WHERE,而在关联字段本身是否干净、索引是否生效、条件是否放对位置。
- 查重复值:
SELECT order_id, COUNT(*) FROM orders GROUP BY order_id HAVING COUNT(*) > 1;同样查order_items表的order_id是否重复 - 检查
NULL:如果ON a.id = b.a_id中任一端含NULL,INNER JOIN会丢行,LEFT JOIN在后续加WHERE b.status IS NOT NULL会悄悄转成INNER JOIN,但执行计划未必优化掉冗余扫描 - 别在
ON里写函数:ON UPPER(a.code) = UPPER(b.code)会让两边索引全失效,优化器被迫选哈希连接,中间结果直接膨胀
EXISTS 替代 LEFT JOIN + WHERE 判断存在性
只要目的是“主表某条记录在从表是否存在”,就别用 LEFT JOIN ... WHERE b.id IS NOT NULL ——它先全量连接再过滤,极易触发笛卡尔积。
- 差写法(膨胀明显):
SELECT o.* FROM orders o LEFT JOIN order_items i ON o.order_id = i.order_id WHERE i.order_id IS NOT NULL - 好写法(半连接,不生成中间行):
SELECT o.* FROM orders o WHERE EXISTS (SELECT 1 FROM order_items i WHERE i.order_id = o.order_id) - 注意:如果还要取
i.product_name或i.qty,就不能只靠EXISTS,得回到JOIN,但此时必须确保order_items(order_id)或(order_id, item_id)有有效索引
先聚合再 JOIN,别让明细全拉出来
要查“每个客户的总金额+最近下单时间”,直接 JOIN 后 GROUP BY 会把所有订单明细行加载进内存;而先各自聚合,再用主键关联,数据量能压到千分之一。
- 低效方式(明细全加载):
SELECT c.name, SUM(i.amount), MAX(o.create_time) FROM customers c JOIN orders o ON c.id = o.cust_id JOIN order_items i ON o.id = i.order_id GROUP BY c.id, c.name - 优化方式(分步聚合):
WITH cust_orders AS (SELECT cust_id, SUM(amount) total_amt, MAX(create_time) last_order FROM orders o JOIN order_items i ON o.id = i.order_id GROUP BY cust_id), cust_info AS (SELECT id, name FROM customers) SELECT ci.name, co.total_amt, co.last_order FROM cust_info ci JOIN cust_orders co ON ci.id = co.cust_id - 关键点:CTE 或派生表提前收窄,避免大表全程参与连接;且每个子查询都应有对应索引支撑(如
orders(cust_id)、order_items(order_id))
复合索引怎么建才真起作用?
索引不是建了就有效,顺序错、类型不一致、函数一裹,立马掉线。
-
ON a.user_id = b.id AND b.status = 'paid',索引必须是(id, status),而不是(status, id)——最左前缀必须匹配JOIN字段的等值条件 - 如果还有
WHERE b.ctime > '2024-01-01',索引建议为(status, ctime, id):高选择性等值字段status放最左,范围条件ctime居中,JOIN字段id放最后 - 常见失效场景:
ON DATE(t1.time) = DATE(t2.date)(函数)、ON t1.uid = t2.user_id但一边是INT一边是BIGINT(隐式转换)、外键字段没手动建索引(约束 ≠ 索引) - 验证是否生效:看
EXPLAIN的type是不是ref或eq_ref,key列是否显示索引名,rows是否显著下降
真正卡住性能的,往往不是 JOIN 本身,而是 JOIN 前没过滤、JOIN 时没索引、JOIN 后才聚合——这些环节任何一个松动,数据量翻倍,执行时间可能翻十倍。别只盯着 SQL 写法,先看执行计划里哪一步 rows 突然暴涨,那里就是病灶。

















