先查右表关联字段重复:SELECT join_column, COUNT() FROM right_table GROUP BY join_column HAVING COUNT() > 1;若结果非空,且COUNT(*)远大于COUNT(DISTINCT left_table.primary_key),或执行计划rows_examined显著超左表行数,即可确认一对多导致翻倍。

怎么确认是不是一对多导致的翻倍
先别改SQL,直接查右表关联字段的重复情况:SELECT join_column, COUNT(*) FROM right_table GROUP BY join_column HAVING COUNT(*) > 1。如果返回结果不为空,说明该字段确实存在一对多——比如user_id在订单表里出现5次,那LEFT JOIN用户表时,对应用户行就会被复制5次。
再对比两个数:COUNT(*) 和 COUNT(DISTINCT left_table.primary_key)。如果前者远大于后者,基本能锁定是JOIN膨胀了。执行计划里若rows_examined明显高于左表总行数,也是强信号。
常见漏检点:
- 右表是视图或子查询?把它单独执行一遍,看输出里有没有重复
join_column - 外键没加
UNIQUE约束?用SHOW CREATE TABLE right_table确认 - 业务上本该唯一,但历史数据有脏数据(如导入错误、逻辑缺陷)
为什么ON条件写在WHERE里会让问题更隐蔽
LEFT JOIN后对右表字段加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再筛,膨胀已发生。
子查询预聚合时最容易漏掉的三个细节
预聚合不是写个GROUP BY就完事。以下三点漏一个,结果照样错:
-
GROUP BY字段必须和外层ON条件里的右表字段完全一致,比如子查询按order_id分组,外层就得ON o.order_id = s.order_id,不能写成ON o.id = s.order_id - LEFT JOIN未匹配时,聚合字段(如
total_amount)会是NULL,SUM(NULL)返回NULL而非0——需要显式写COALESCE(SUM(...), 0) - 子查询里忘了加业务过滤,比如只统计有效订单,却漏了
WHERE status != 'cancelled',导致聚合基数偏大
什么时候不该硬上预聚合
如果右表重复不是数据问题,而是业务本就如此(比如一个用户真有多个角色、多个地址),那翻倍反而是正确反映事实。此时强行预聚合等于丢信息。
更合理的做法可能是:
- 用
JSON_AGG()把多行打包成JSON数组(PostgreSQL/MySQL 5.7+支持) - 用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC)取最新一条,再QUALIFY = 1过滤(Snowflake/BigQuery适用) - 或直接接受宽表结构,在应用层处理重复主表字段
真正难的从来不是写出GROUP BY子查询,而是判断:这个“多”,是脏数据、设计缺陷,还是业务真相。










