LEFT JOIN后COUNT/SUM翻倍是因JOIN阶段物理行膨胀所致,非SQL错误;应通过子查询预聚合解决,且必须满足三条件:带别名、GROUP BY关联键、过滤下推至子查询内。

为什么LEFT JOIN后COUNT/SUM一定翻倍
不是SQL写错了,是数据在JOIN阶段就物理膨胀了。比如一个用户关联3个标签,users LEFT JOIN user_tag会生成3行重复的用户记录;COUNT(*)对这3行各加1,结果就是3,而非“该用户有3个标签”这个逻辑事实。执行SELECT user_id, COUNT(*) FROM users u LEFT JOIN user_tag ut ON u.id = ut.user_id GROUP BY u.id就能验证:只要某user_id出现多次,说明已膨胀。
子查询预聚合必须满足的三个硬性条件
漏掉任一条件,轻则报错,重则结果错但不报错。
-
SELECT子查询必须带AS alias_name别名,MySQL报Error Code: 1248. Every derived table must have its own alias就是这个原因;别名不能是order、group等保留字 - 子查询内必须
GROUP BY关联键(如user_id),且只返回该键 + 聚合字段(如COUNT(*) AS tag_count),多一个字段都可能让外层JOIN失效 - 外层
ON条件的字段类型必须完全一致:主表id是BIGINT,子查询里的user_id也必须是BIGINT;用CAST或隐式转换会让索引失效,预聚合白做
WHERE条件必须写在子查询内部
把过滤条件挪到外层WHERE,等于主动把LEFT JOIN降级为INNER JOIN。例如统计“有效标签数”,WHERE is_active = 1若写在外层,所有is_active IS NULL或is_active = 0的用户都会被整行剔除,而不是保留0值。
正确写法示例:
SELECT u.id, u.name, COALESCE(t.tag_count, 0) AS tag_count FROM users u LEFT JOIN ( SELECT user_id, COUNT(*) AS tag_count FROM user_tag WHERE is_active = 1 GROUP BY user_id ) AS t ON u.id = t.user_id;
什么时候预聚合反而更慢?避开相关子查询
相关子查询(correlated subquery)是性能杀手,例如:SELECT (SELECT COUNT(*) FROM user_tag WHERE user_id = u.id) FROM users u。数据库会对主表每行执行一次子查询——10万用户 = 10万次独立扫描,比JOIN还慢。
真正安全的预聚合只有一种形态:非相关、带别名、GROUP BY唯一键、过滤下推。如果业务需要同时返回聚合值和明细字段(如最新打标时间),预聚合就不够用了,得换LATERAL或窗口函数。

















