聚合结果虚高是因为JOIN阶段数据膨胀,GROUP BY无法修复;执行顺序为FROM→JOIN→WHERE→GROUP BY→SELECT,笛卡尔积发生在JOIN阶段,导致SUM/COUNT基于膨胀后行数计算。

聚合结果翻倍不是函数算错了,是JOIN阶段数据已经膨胀了——GROUP BY无法修复已错乱的中间行数。
为什么 COUNT/SUM 会虚高
执行顺序是 FROM → JOIN → WHERE → GROUP BY → SELECT。笛卡尔积发生在 JOIN 阶段,GROUP BY 只是对膨胀后的物理行做汇总。比如订单表 1 万行 × 订单明细表平均 5 行 = 中间结果 5 万行,SUM(amount) 就加了 5 万次,不是 1 万次。
- 查
EXPLAIN的rows列:如果orders表本身 1 万行,但JOIN后预估行数跳到 50 万,基本就是膨胀了 - PostgreSQL 看到
Nested Loop (Join Filter: true),MySQL 看到type: ALL + Extra: Using join buffer,都是漏ON条件的信号 -
DISTINCT对SUM无效:SUM(DISTINCT amount)是去重金额值,不是去重行;真正要的是“每order_id只贡献一行明细”
LEFT JOIN 中 WHERE 和 ON 的位置不能乱放
放错位置等于主动制造膨胀。关键区别在于:放 WHERE 是先全量配对再过滤;放 ON 是从源头控制右表参与连接的行数。
- 错误写法:
LEFT JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active'→ 先把所有客户(含status IS NULL)都 JOIN 进来,再踢掉非 active 行,中间已膨胀 - 正确写法:
LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active'→ 右表只拉status = 'active'的记录,没膨胀 - 如果业务要求保留无客户匹配的订单,又只取活跃客户信息,
WHERE绝对不能碰右表字段,否则LEFT JOIN就退化成INNER JOIN
子查询预聚合才是根治方案
让多端表先按关联键压缩成单行,再和主表拼接。这不是加个 DISTINCT 或改写 WHERE 能糊弄过去的。
- 子查询必须严格
GROUP BY关联字段,比如order_items按order_id聚合,外层ON就得用o.id = oi.order_id - 过滤条件(如
status = 'paid')必须写在子查询里,不能挪到外层WHERE,否则子查询输出仍含无效行 - 示例(统计每个用户的订单总额):
SELECT u.id, u.name, COALESCE(o.total_amount, 0) AS total_amount FROM users u LEFT JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE status = 'paid' GROUP BY user_id ) o ON u.id = o.user_id
最容易被忽略的复杂点
膨胀不只发生在显式 CROSS JOIN 或漏 ON 的场景里;当右表对左表主键不唯一(比如一个订单对应多条物流记录、多个优惠券),即使写了 ON,只要没预聚合或限制右表行数,照样翻倍。这时候看执行计划里的实际扫描行数,比看最终结果行数更关键。











