LEFT JOIN后SUM或COUNT翻倍是因为一对多关联导致主表一行被复制成N行参与计算,是JOIN的必然结果而非Bug;需通过子查询预聚合(如SELECT user_id, SUM(amount) FROM orders GROUP BY user_id)再JOIN主表,并确保GROUP BY字段与外层JOIN键一致,同时将过滤条件置于子查询内而非外层WHERE。
为什么LEFT JOIN后SUM或COUNT会翻倍
因为一对多关联时,主表一行被复制成N行参与后续计算——这不是数据库出错,是JOIN的必然结果。比如
LEFT JOIN
(1用户→N订单),再直接
,金额就被累加了N次。
常见现象:
比预期高几倍、
明显偏大、分组后行数异常增多。执行计划里若某步
突增10倍以上,基本就是膨胀源。
先确认是否真是一对多:用
看分布
别只看最终
后的行数——膨胀发生在它之前
只能救急,且只对
有效;
语义错误,不能用
子查询预聚合的标准写法
核心是让“多”侧表先按关联键压缩成单行,再和主表
。子查询必须返回唯一键+聚合结果,不带歧义字段。
例如统计每个用户的订单总金额:
不可省,否则子查询变成交叉连接
子查询必须有别名(如
),否则MySQL报
把无订单用户的
转为0,避免前端异常
若需加条件(如只算已支付订单),必须写在子查询内部:
,不能丢到外层
ON里放条件 vs WHERE里放条件的区别
这是物理执行顺序问题,不是语义区别。放错位置会让膨胀提前发生,子查询也救不了。
错误写法(先全量JOIN,再过滤):
正确写法(从源头控右表基数):
前者先JOIN出所有客户行,再踢掉非active和NULL,膨胀已发生
后者右表只拉符合条件的行进来,JOIN前就控量
如果右表本身有重复(如历史客户含多条同id记录),光靠
不够,必须先子查询去重或取最新一条
什么时候子查询预聚合也不够用
它只解决“只要聚合值”的需求。一旦需要原明细中的非分组字段(比如最新订单的
或
),子查询就力不从心。
此时得换方案:
PostgreSQL可用
,MySQL 8.0+可用窗口函数+
标记后过滤
需要“每个订单的最新支付时间 + 所有明细总金额”,就不能三表直连
,得用窗口函数分层计算
子查询里漏了个
,结果还是错的——真正难的不是写法,是贴着业务逻辑抠清楚:哪张表该聚合、按什么字段、要不要加状态或时间范围
usersordersSUM(orders.amount)COUNT(*)SUM()rowsSELECT user_id, COUNT(*) FROM orders GROUP BY user_idGROUP BYDISTINCTCOUNT(DISTINCT id)SUM(DISTINCT amount)JOINSELECT u.id, u.name, COALESCE(t.total_amount, 0) AS total_amount
FROM users u
LEFT JOIN (
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
) t ON u.id = t.user_id;GROUP BY user_idtError Code: 1248COALESCE(t.total_amount, 0)NULLWHERE status = 'paid'WHERELEFT JOIN customers c ON o.customer_id = c.id
WHERE c.status = 'active'LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active'ONstatuscreated_atLATERALROW_NUMBER()GROUP BYAND deleted = 0