聚合结果偏大必因JOIN行膨胀,先查右表关联字段是否重复,再用预聚合子查询(GROUP BY关联键)切断膨胀链,过滤条件须置于子查询内而非WHERE或ON外。

聚合结果比实际数据偏大,几乎可以断定是 JOIN 导致的行膨胀——不是计算错,而是算多了。
查右表关联字段是否重复
先别改 SQL,直接验证右表(被 JOIN 的那张)关联字段有没有一对多。比如你 LEFT JOIN orders ON users.id = orders.user_id,那就查 orders.user_id 是否重复:
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) > 1;
如果返回结果不为空,且某些 user_id 出现几十甚至上百次,基本就是它了。再对比两个数:COUNT(*) 和 COUNT(DISTINCT users.id),前者远大于后者,就是膨胀铁证。
- 右表是视图或子查询?单独执行一遍,看输出里有没有重复
user_id -
SHOW CREATE TABLE orders看user_id字段有没有加UNIQUE或外键约束 - 业务上本该唯一,但历史脏数据(比如导入错误)也可能导致重复
LEFT JOIN 后 WHERE 过滤让问题更隐蔽
把右表的过滤条件写在 WHERE 里,比如 WHERE o.status = 'paid',会导致 LEFT JOIN 退化成 INNER JOIN:没匹配上或状态不符的左表行全被踢掉。你以为“数据变少了”,其实是“翻倍被掩盖了”。
正确做法是把条件塞进 ON:
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
这样左表全保留,右表只拉符合条件的行进来,从源头控量。
-
AND o.status = 'paid'必须写在ON里才生效;写在WHERE里,优化器会先完成全量 JOIN 再筛,膨胀已发生 - 执行计划中
rows_examined明显高于左表总行数,也是强信号
用子查询预聚合切断膨胀链
核心是让多端表先“压缩”成每关联键一行,再和主表拼接。这样主表每行只匹配 1 行聚合结果,SUM 和 COUNT 才回归本意。
示例(统计每个用户的订单总额):
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;
- 子查询必须严格
GROUP BY关联字段(如user_id),漏掉就等于没做 - 外层
JOIN条件必须和子查询GROUP BY字段完全一致 - 记得用
COALESCE(o.total_amount, 0)处理未匹配时的NULL,否则前端可能崩 - 过滤条件(如
status = 'paid')必须放在子查询里,不能丢到外层
别指望 DISTINCT 救 SUM 和 AVG
COUNT(DISTINCT order_id) 可以绕过重复计数,但它对 SUM、AVG、MAX 完全无效。SUM(DISTINCT amount) 是去重金额值,不是去重订单行,业务语义全毁。
-
DISTINCT在大数据量下需哈希去重,比预聚合更慢,且无法利用索引 - 窗口函数(如
SUM() OVER (PARTITION BY user_id))也不解决根本问题——它只是在膨胀后的结果集上广播计算,SUM 依然算错 - 真正难的不是写出预聚合,而是意识到:只要右表对左表主键不唯一,就必须在 JOIN 前把它“压扁”

















