left join后sum翻倍不是数据库错误,而是join先笛卡尔展开再聚合所致:1行主表×n行子表=n行物理结果,sum重复累加;应通过子查询按关联键预聚合、过滤条件内置、coalesce处理null来修复。

为什么LEFT JOIN后SUM()一定翻倍
不是数据库算错,是JOIN先展开数据再聚合:1 行用户 × 3 行订单 = 3 行物理结果,SUM(o.amount) 就真把同一笔订单金额加了 3 次。执行计划里 rows_examined 明显大于左表行数(比如 users 表 10 万行,扫描 30 万行),基本可锁定膨胀。
子查询预聚合必须写对这三点
核心动作是让多端表(如 orders)先按关联键压成一行,再拼主表。漏掉任一细节都会失效:
-
GROUP BY字段必须和外层ON条件完全一致(比如子查询GROUP BY user_id,外层就得写ON u.id = o.user_id,不能是o.uid或隐式类型转换) - 过滤条件(如
WHERE status = 'paid')必须放在子查询内——放外层WHERE会先撑开再过滤,膨胀已发生 -
LEFT JOIN未匹配时,聚合字段为NULL,SUM(NULL)返回NULL,不是 0;必须用COALESCE(o.total_amount, 0)
别碰DISTINCT,它救不了SUM()
SUM(DISTINCT amount) 去的是数值重复,不是行重复。两笔不同订单都是 100 元,就会被当做一个值加一次,业务逻辑全毁。同样,COUNT(DISTINCT order_id) 只对计数类有效,且掩盖了聚合粒度错误。大数据量下 DISTINCT 还要哈希去重,比预聚合更慢、无法走索引。
需要明细字段时改用窗口函数
如果既要“每个用户的最新订单时间”,又不能让 orders 表拖着 users 膨胀,就得绕过 JOIN:
-
FIRST_VALUE(o.created_at) OVER (PARTITION BY o.user_id ORDER BY o.created_at DESC)把最新时间广播到每行,不新增物理行 -
ROW_NUMBER() OVER (PARTITION BY o.user_id ORDER BY o.created_at DESC) = 1配合QUALIFY(Snowflake/BigQuery)或 CTE(MySQL 8.0+/PostgreSQL)精准取最新一条 - 注意:窗口函数不解决聚合失真本身;若最终还要
GROUP BY users.id,仍得退回预聚合子查询
AND deleted = 0 这类业务过滤——这些都得贴着原始业务逻辑抠,写完必须拿单条用户数据手算验证。










