left join后count(*)统计的是关联组合行数而非主表行数,因右表非唯一导致行膨胀;正确做法是子查询预聚合,按关联字段group by并在子查询内加过滤条件。

因为JOIN操作本身会按关联关系展开行数,COUNT(*)统计的是物理行,不是业务逻辑行——只要右表对左表主键不唯一,1 行主表就可能变成 N 行,COUNT(*)自然就是 N。
LEFT JOIN 后 COUNT(*) 为什么不是“主表行数”
它根本没在数“有多少用户”,而是在数“有多少用户-订单组合”。比如 users 表 1 行,orders 表里有 3 条该用户的记录,LEFT JOIN 后就生成 3 行,COUNT(*) 返回 3。
- 常见错误现象:
SELECT u.id, COUNT(*) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id返回的数字远大于 1,但你以为是在查“每个用户有没有下单” - 真正想统计“用户数”,应该用
COUNT(DISTINCT u.id);想统计“订单数”,应先确保 orders 是聚合后的结果再 JOIN - 执行计划里如果
rows_examined显著高于左表原始行数(比如 users 10 万行,却扫描了 45 万行),基本可确认是膨胀导致
子查询预聚合才是治本解法
不能靠 DISTINCT 或调整 GROUP BY 位置来“掩盖”问题,必须在 JOIN 前把多端数据压缩成每关联键一行。
- 子查询必须严格
GROUP BY关联字段(如user_id),漏掉就等于没做 - 外层
ON条件必须和子查询GROUP BY字段完全一致,比如子查询GROUP BY o.user_id,外层就得写ON u.id = o.user_id - 过滤条件(如
status = 'paid')必须写在子查询内,否则无效;放在外层WHERE会导致先膨胀再过滤 - 示例:统计每个用户的有效订单数
SELECT u.id, u.name, COALESCE(o.order_cnt, 0) AS order_cnt FROM users u LEFT JOIN ( SELECT user_id, COUNT(*) AS order_cnt FROM orders WHERE status = 'paid' GROUP BY user_id ) o ON u.id = o.user_id;
COUNT(DISTINCT ...) 只能救急,不能替代逻辑修复
COUNT(DISTINCT order_id) 在某些场景下能绕过重复计数,但它只适用于“个数类”聚合,且容易掩盖真实数据问题。
-
COUNT(DISTINCT order_id)有效:你想知道“这个用户下了几单”,而 orders 表已和明细表 JOIN 过 -
SUM(DISTINCT amount)无效:金额去重后语义全毁,同一用户两笔 100 元订单会被算成一次 100 元 -
DISTINCT是最终输出层动作,无法阻止 JOIN 阶段的行复制,性能也更差(需哈希去重,无法走索引) - 如果子查询里漏了
AND deleted = 0或时间范围,COUNT(DISTINCT ...)依然会错
最易被忽略的一点:判断哪张表该聚合、按什么字段聚合,不是看表名,而是看业务语义中“谁是维度、谁是事实”。users 是维度,orders 是事实;但如果 orders 已经是汇总表(比如按日聚合),那它就不再是“多端”,强行再聚合反而会出错。











