先聚合再关联能显著提速,核心是避免中间结果集爆炸:先join后group by会生成海量匹配行再聚合,而先按维度聚合可大幅压缩数据量,再关联维表,总数据量可控。

先聚合再关联,通常比先关联后聚合快得多,尤其当被关联表数据量大、聚合维度又窄时。
为什么先聚合再关联能显著提速
核心在于避免“中间结果集爆炸”:如果先 JOIN 再 GROUP BY,数据库得先把所有匹配行生成出来(比如 100 万订单 × 每单 5 条明细 = 500 万行),再在这 500 万行上做 SUM/COUNT;而先对明细表按 order_id 聚合,可能只产出 100 万行汇总结果(每单一行),再 JOIN 用户表,总数据量直接压到百万级以内。
常见失效场景包括:
- 明细表无有效索引支撑聚合字段(如
GROUP BY order_id但order_id上没索引) - 聚合后未显式限制结果集,导致后续 JOIN 仍面对超大中间集
- 用了
SELECT *在聚合子查询里,带入大量无用字段,拖慢内存和网络
怎么写才真正生效:子查询聚合 + 显式字段控制
关键不是套个 SELECT * FROM (SELECT ...) t 就完事,而是让聚合子查询本身轻量、可索引、结果可控。
正确写法要点:
- 聚合子查询只 SELECT 必需的分组键和聚合值,例如:
SELECT order_id, SUM(amount) AS total_amount FROM order_items GROUP BY order_id - 若后续要 JOIN 主表,确保聚合子查询的分组键(如
order_id)在主表上有对应索引 - 必要时加
LIMIT或高选择性WHERE下推(如只聚合近 30 天数据) - 避免在聚合子查询里用
HAVING过滤大量行——它发生在聚合之后,无法减少扫描量;应优先用WHERE提前筛
示例对比:
-- ❌ 先关联后聚合(慢) SELECT u.name, COUNT(o.id), SUM(oi.amount) FROM users u JOIN orders o ON u.id = o.user_id JOIN order_items oi ON o.id = oi.order_id GROUP BY u.name;
-- ✅ 先聚合再关联(快) SELECT u.name, t.cnt, t.total FROM users u JOIN ( SELECT o.user_id, COUNT(*) AS cnt, SUM(oi.amount) AS total FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.created_at >= '2026-04-01' -- 下推时间过滤 GROUP BY o.user_id ) t ON u.id = t.user_id;
EXPLAIN 验证是否真走“先聚后关”路径
执行 EXPLAIN 后,重点看两处:
- 聚合子查询是否显示
type为ref或range(说明走了索引),而非ALL - 外层 JOIN 的
rows估算值,是否接近子查询的聚合后行数(比如 1 万),而不是明细表原始行数(比如 100 万) - 若出现
Using temporary; Using filesort在外层,说明聚合逻辑没被下推,或 GROUP BY 字段没索引
特别注意:MySQL 5.7 及更早版本对子查询优化较弱,某些写法(如相关子查询)可能无法下推;8.0+ 对 CTE 和派生表优化更好,但依然建议用显式子查询而非 CTE,除非需复用多次。
容易被忽略的细节:NULL 值与空聚合结果处理
聚合子查询若返回空结果(如某用户无订单),外层 JOIN 会直接丢掉该用户。若业务要求保留(如显示 0 订单),必须改用 LEFT JOIN,并用 COALESCE(t.cnt, 0) 补零。
另一个坑是:聚合字段含 NULL 时,SUM() 和 COUNT() 行为不同——COUNT(*) 统计行数,COUNT(col) 自动忽略 col 为 NULL 的行。若明细表中 amount 允许 NULL,且你希望把 NULL 当 0 算,得写 SUM(COALESCE(oi.amount, 0)),否则结果会偏低。
真正卡住性能的,往往不是语法多难,而是聚合边界没划清、NULL 没兜住、索引没对齐这三处。










