group by在join之后执行,仅对已膨胀的中间结果聚合,无法消除笛卡尔积;正确做法是将右表过滤条件移至on子句,并对多侧表用子查询按关联键预聚合。

GROUP BY 执行时机在 JOIN 之后
数据库执行顺序是 FROM → JOIN → WHERE → GROUP BY → SELECT。笛卡尔积膨胀发生在 JOIN 阶段,GROUP BY 只是对已经膨胀的中间结果做聚合,不是“去重开关”。它不会删行,也不会压缩重复组合——只会把 10 行订单明细(本该属于 1 个订单)强行 SUM 成一个值,但这个 SUM 已经加了 10 遍。
LEFT JOIN 中 WHERE 过滤右表字段等于主动放行膨胀
这是最隐蔽也最常踩的坑。写成:
LEFT JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active'
等价于:先让所有 customers(含 status IS NULL)都和订单配一遍,生成可能几十万行中间结果,再用 WHERE 把不满足的全踢掉。膨胀早已发生,GROUP BY 只能对残局收尾。
- 正确做法是把过滤条件塞进
ON:LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active' - 这样右表只拉符合条件的记录,JOIN 输出行数从“主表 × 全量右表”降到“主表 × 活跃客户数”
- 如果业务要求保留无客户匹配的订单,又必须过滤右表,
WHERE碰右表字段就等于放弃 LEFT 语义
多个一对多表直接 JOIN 必然引发交叉组合
比如 orders ← order_items(1:N)← order_payments(1:N),三者直接连:
FROM orders o LEFT JOIN order_items i ON o.id = i.order_id LEFT JOIN order_payments p ON o.id = p.order_id
若某订单有 3 条明细、2 笔支付,中间结果就是 3 × 2 = 6 行。这不是语法错误,是逻辑必然。
- 解决路径不是调
GROUP BY字段,而是提前收拢:把order_items按order_id聚合成一行(如SUM(amount)),order_payments同理 - 子查询必须严格
GROUP BY关联键,漏掉就变新笛卡尔积:(SELECT order_id, SUM(amount) FROM order_payments GROUP BY order_id) - 别在子查询里加
ORDER BY或LIMIT,优化器可能无法下推,导致全表扫描
EXPLAIN 中 rows 暴涨才是真信号,别只盯最终行数
GROUP BY 后看着只有 1 万行,不代表没问题。关键看 EXPLAIN 的 rows 列:如果 orders 表本身 1 万行,但 JOIN 后预估扫描 80 万行,基本就是膨胀了。
- PostgreSQL 看到
Nested Loop (Join Filter: true),基本等于确认漏了ON条件 - MySQL 看到
type: ALL+Extra: Using join buffer,尤其出现在第二张或第三张表上,就是危险信号 - 用窗口函数快速验证:
COUNT(*) OVER (PARTITION BY o.id)能直接看出单个订单拉出多少行明细,比猜快得多
GROUP BY,是判断哪张表该聚合、按什么字段聚合、过滤条件该写进子查询还是外层——这些全得贴着业务数据质量抠。一不留神,子查询里漏了个 AND deleted = 0,结果照样翻倍。










