LEFT JOIN右表多条匹配必然产生重复行,因语义要求左表每行与右表所有匹配行组合;应通过子查询预聚合或ROW_NUMBER()取单行解决,而非DISTINCT。

为什么LEFT JOIN右表多条匹配必然产生重复行
这不是SQL写错了,是LEFT JOIN的语义决定的:左表一行只要在右表找到N条满足ON条件的记录,就会生成N行结果。比如users表1条用户数据,关联到orders表的3条订单,结果里就出现3次该用户信息——数据库没“错”,它只是忠实地执行了集合运算。
常见误判点:COUNT(*)统计结果行数当成用户数;后续再JOIN第三张表时,拿这膨胀后的结果当主表,错误级联放大;看到重复就加DISTINCT,却没意识到它只对最终字段组合去重,不解决逻辑膨胀。
用子查询预聚合右表,最常用也最可控
核心思路:不让右表“原样上桌”,而是先按连接键聚合出你需要的那一行(如最新时间、最大值、计数等),再JOIN。这样既避免重复,又保留业务语义。
- 要取每个用户的最新登录IP:
SELECT u.id, u.name, p.max_ip<br>FROM users u<br>LEFT JOIN (<br> SELECT user_id, MAX(ip_addr) AS max_ip<br> FROM t_log<br> GROUP BY user_id<br>) p ON p.user_id = u.id
- 确保子查询里的
GROUP BY字段和外层ON条件完全一致,否则关联失效 - 过滤条件必须下推到子查询内,例如只统计近30天日志,得写
WHERE log_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)在子查询里,不能放外层
用ROW_NUMBER()筛出右表单行,适合取“最新/最高/最低”类场景
当你要的是右表某一条代表记录(比如最新合同、最后一次操作),ROW_NUMBER()比聚合更精准,且不丢失非聚合字段。
- 给每组右表记录编号,再只取
rn = 1:SELECT u.id, u.name, p.ip_addr, p.login_time<br>FROM users u<br>LEFT JOIN (<br> SELECT user_id, ip_addr, login_time,<br> ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time DESC) AS rn<br> FROM t_log<br>) p ON p.user_id = u.id AND p.rn = 1
-
PARTITION BY必须和ON中的连接键一致,否则编号逻辑错乱 - ORDER BY里字段要有索引,否则窗口函数性能会陡降
- 注意
LEFT JOIN ... AND p.rn = 1不能写成WHERE p.rn = 1,否则会退化为INNER JOIN
DISTINCT不是解法,是临时止血手段
DISTINCT只应在明确知道语义正确、且无需聚合计算、结果集不大时使用。它不改变JOIN逻辑,只是事后擦除整行重复,代价高、掩盖问题。
- 只对真正需要展示的字段用
DISTINCT,别写SELECT DISTINCT * - 如果后续要
SUM(r.buy_num * r.goods_price),DISTINCT毫无作用——价格字段仍被重复计入 - 加了
ORDER BY或LIMIT时,MySQL往往得先生成全部中间结果再过滤,内存和临时表压力陡增 - 大表上跑前,先确认连接字段(如
r.organization_id和org.organization_id)是否有索引,否则可能触发全表扫描+文件排序
真正难处理的,是那些你本以为“一对一”、结果发现右表连接键实际不唯一的情况——比如历史快照表漏了effective_date过滤,或维度表本身有脏数据。这种问题不会在EXPLAIN里直接报错,但rows列会远超预期,得靠COUNT(*)和COUNT(DISTINCT left_id)对比膨胀倍数来揪出来。

















