“先JOIN再GROUP BY”会让查询变慢十倍,因为优化器常生成笛卡尔积级中间结果,导致IO、内存和哈希开销暴增;应改用CTE或子查询先聚合再JOIN,并确保过滤下推、索引到位、LEFT JOIN条件置于ON中。

为什么“先JOIN再GROUP BY”会让查询变慢十倍
因为数据库优化器常把你的SQL理解成“先把几百万订单和用户全拼起来,再分组”,而不是你写的“先按用户汇总订单,再连用户信息”。中间结果集可能从500万行暴涨到上千万甚至上亿行,磁盘IO、内存占用、哈希分组开销全部拉满。
典型表现是:EXPLAIN里出现Using temporary和Using filesort,rows_examined数值远超单表行数。子查询单独跑1秒,一加JOIN就30秒以上,基本就是这个原因。
- 一对多关系(如1个用户对应N个订单)下,JOIN会生成笛卡尔积级中间行
- 优化器缺乏统计信息或索引时,容易放弃你写的子查询结构,改走嵌套循环或哈希连接
- 如果关联字段没索引、字符集不一致(比如一边
utf8mb4_0900_ai_ci,一边utf8_general_ci),隐式转换会让索引失效,进一步诱发全表扫描
怎么写才能真正实现“先聚合再JOIN”
核心是让聚合逻辑在子查询或CTE中封闭完成,且子查询输出必须只含关联键+聚合结果,不能带无关字段。外层JOIN只负责补维度信息。
正确示例(MySQL 8.0+/PostgreSQL):
WITH order_summary AS ( SELECT user_id, COUNT(*) AS cnt, SUM(amount) AS total FROM orders WHERE created_at >= '2026-03-22' GROUP BY user_id ) SELECT u.name, u.level, COALESCE(o.cnt, 0), COALESCE(o.total, 0) FROM users u LEFT JOIN order_summary o ON u.id = o.user_id;
- 过滤条件(如时间范围)必须下推到子查询里,不能留在外层
WHERE - 用
LEFT JOIN而非INNER JOIN,优化器更倾向保留左表驱动顺序 - 确保
orders.user_id有索引;若子查询还涉及WHERE,考虑建复合索引如INDEX(user_id, created_at) - 避免子查询返回
NULL的user_id,否则ON u.id = o.user_id会漏匹配——加WHERE o.user_id IS NOT NULL或确认该字段为NOT NULL
COUNT(*) 和 COUNT(列) 的坑,直接导致结果错
很多人发现“未下单用户显示订单数=1”,问题就出在这里:COUNT(*)统计的是LEFT JOIN后左表的行数,哪怕右表没匹配,这行还在;而COUNT(orders.id)只算非NULL值,空关联才得0。
- 统计订单数量,必须用
COUNT(orders.id)或COUNT(orders.order_id),不能用COUNT(*) - 确保所选列是主键或明确
NOT NULL;COUNT(orders.status)有风险,因该字段可能为NULL - MySQL旧版本宽松模式下,
COUNT(某NULLable列)行为不可靠,建议统一用主键
LEFT JOIN的WHERE条件放错位置,等于白写
LEFT JOIN本意是保左表全量,但一旦把业务条件(如时间、状态)写进外层WHERE,数据库会在JOIN之后再过滤,把没匹配的左表行全踢掉。
错例:LEFT JOIN orders o ON u.id = o.user_id WHERE o.created_at >= '2026-01-01' → 只返回有2026年订单的用户
对例:LEFT JOIN orders o ON u.id = o.user_id AND o.created_at >= '2026-01-01' → 用户全在,无订单者计数为0
- 所有业务过滤条件(包括
status = 'paid'、type IN ('web', 'app'))必须全部塞进ON子句 - 不能拆一半到
WHERE,也不能用HAVING替代——HAVING是在聚合完才筛,性能损失已发生 - 如果需在外层再筛用户属性(如
u.level = 'vip'),必须同步下推到子查询的WHERE,否则会多算、结果错
实际执行时最容易被忽略的,是子查询里没加WHERE过滤就直接GROUP BY,或者关联字段缺索引却没意识到——这两点会让“先聚合”策略彻底失效,性能反而比原来更差。











