LEFT JOIN 产生重复行是因一对多关系触发隐式笛卡尔积;用 GROUP BY + 聚合函数或关联子查询才是正解,DISTINCT 仅适合简单去重且有性能代价。

重复不是 bug,是 JOIN 的正常行为;但不处理就会让 COUNT、SUM 算错,甚至前端展示错乱。
为什么 LEFT JOIN 一查就多出好几倍行数
根本原因是“一对多”关系触发了隐式笛卡尔积。比如 users 表 1 条用户记录,关联到 orders 表的 5 条订单,LEFT JOIN 后这 1 条用户数据就会被复制 5 次——每条都配一个订单。这不是语法写错了,而是数据库在按集合逻辑忠实执行关联。
- 检查
ON条件是否指向右表的唯一字段(如主键或带UNIQUE约束的列),否则很容易踩中这个坑 -
LEFT JOIN system_organization_purchase_contract org ON org.organization_id = r.organization_id这种写法,如果org.organization_id不是唯一值,就必然重复 - 用
EXPLAIN看执行计划,重点关注rows列是否远超左表实际行数,这是重复的早期信号
DISTINCT 能快速止血,但有隐藏代价
DISTINCT 是最直接的去重手段,适合只读查询、结果集不大、且不需要聚合计算的场景。但它是在所有字段组合层面去重,一旦你加了 ORDER BY 或 LIMIT,MySQL 往往得先生成全部中间结果再过滤,内存和临时表压力会陡增。
- 只对真正需要展示的字段用
DISTINCT,别写SELECT DISTINCT * - 如果后续要
SUM(r.goods_price),DISTINCT无法解决聚合失真问题——它只是让行变少,但价格字段本身还是被重复计入了 - 在大表上跑
DISTINCT前,先确认r.organization_id和org.organization_id是否有索引,否则可能触发全表扫描+文件排序
GROUP BY + 聚合函数才是处理一对多的正解
当你要统计、汇总,或者必须保留主表一行一记录时,GROUP BY 是更可控的选择。它强制以主表字段为分组依据,把“多”的部分收进聚合函数里,逻辑清晰且性能可预期。
- 用
GROUP_CONCAT(org.organization_short_name)合并多个组织名,避免丢失信息 - 用
MAX(org.organization_short_name)或MIN()取单个代表值(前提是业务允许) - 若需计算金额总和,直接写
SUM(r.buy_num * r.goods_price),不会因 JOIN 重复而翻倍 -
GROUP BY r.id必须覆盖所有非聚合字段,否则 MySQL 8.0+ 会报错(ONLY_FULL_GROUP_BY模式默认开启)
子查询比 JOIN 更干净的两种情况
当你只需要“多”表里的某个标量值(比如最新合同编号、最高折扣率),而不是整行数据时,子查询往往比 JOIN 更轻量、语义更明确,也天然规避重复。
- 把
LEFT JOIN换成关联子查询:(SELECT org_short_name FROM system_organization_purchase_contract WHERE organization_id = r.organization_id AND tenant_id = 1 LIMIT 1) - 如果右表数据量大,记得给
organization_id和tenant_id加联合索引,否则子查询会变慢 - 慎用
IN (SELECT ...)替代JOIN,MySQL 5.7 及以前版本可能退化为 N+1 查询
真正难处理的不是“怎么去重”,而是“去重之后,那些被压平的明细数据还够不够支撑业务逻辑”。比如报表要同时展示用户总订单数和最新一笔订单时间,就得在 GROUP BY 里用 COUNT(*) 和 MAX(order_time) 分开算——这时候字段含义和聚合粒度必须严格对齐,稍一疏忽,数字就对不上。


















