中间结果集爆炸的头号原因是先JOIN后聚合。三表JOIN使10万订单×5明细×1用户地址生成500万行,再聚合;应改用子查询预聚合,只保留分组键和聚合值,确保索引匹配、过滤下推、LEFT JOIN条件写入ON,并用COALESCE处理NULL。

为什么中间结果集会爆炸——先JOIN后聚合是头号原因
三张表一JOIN,10万订单 × 每单5条明细 × 每个用户1个地址,轻松生成500万行中间数据,再COUNT、SUM,数据库得在内存或磁盘上扛着这500万行跑完聚合。这不是SQL写错了,是执行路径选错了:聚合本该在JOIN前完成,而不是等所有组合拼出来再算。
用子查询预聚合,但必须控制字段和索引
把明细表的聚合逻辑单独拎出来,只保留JOIN必需的字段,否则等于白干:
-
SELECT order_id, SUM(amount) AS total—— 只要分组键和聚合值,别写SELECT *或带created_at这种无关字段 - 确保子查询的
GROUP BY order_id字段,在主表(如orders)上有对应索引,类型也得一致(比如都是BIGINT,不是VARCHAR) - 时间范围过滤必须下推到子查询里:
WHERE created_at >= '2026-01-01',别等外层再筛
LEFT JOIN右表时,WHERE条件写错=变INNER JOIN
这是最隐蔽的“结果集变小”假象,实际是逻辑错误导致有效行被误删:
- 写
LEFT JOIN users u ON o.user_id = u.id WHERE u.status = 'active',会让所有u.id IS NULL的订单直接消失 - 正确做法是把右表过滤移到
ON里:LEFT JOIN users u ON o.user_id = u.id AND u.status = 'active' - 如果右表字段可能为NULL,聚合前必须
COALESCE(u.status, 'unknown'),否则GROUP BY会把NULL当一个独立分组,还可能漏掉统计
EXPLAIN里看两处,才能确认真优化了
别光看有没有用上索引,重点盯这两个地方:
- 子查询部分的
type要是ref或range,不是ALL;rows估算值应接近聚合后行数(比如10万),而不是明细表原始行数(比如500万) - 外层JOIN的
rows列,应该和子查询输出行数量级一致;如果还是显示500万,说明子查询没生效,可能是字段类型不匹配或隐式转换拖了后腿
真正难的不是写出预聚合SQL,而是业务维度是否稳定——比如“近30天活跃用户”定义一变,整个子查询的WHERE就得重审,不然数据就偏了。










