join重复是语义必然,非bug;去重需先明确目标:聚合值、单条明细或存在性判断;left join行数暴增因右表一对多未收敛;count(*)统计join结果会误计;distinct按整行去重;取最新明细用row_number()在on中过滤;聚合须在join前完成;字段类型不一致致隐式转换失败;仅需判断存在性时优先用exists。

JOIN 查询出现重复数据不是 bug,而是语义必然结果;去重不能靠盲目加 DISTINCT,得先明确你要的是聚合值、单条明细,还是存在性判断。
LEFT JOIN 后行数暴增,是因为右表一对多没收敛
比如 users 表 100 行,t_log 表里一个 user_id 出现了 5 次,LEFT JOIN 后这用户信息就会被复制 5 次。数据库完全按规范执行,没出错。
- 直接用
COUNT(*)统计 JOIN 结果,会把“1 个用户 × 5 条日志”算成 5 行,误当 5 个用户 -
DISTINCT对整行去重,只要log_time或ip_addr有一列不同,就不会合并 - 后续再 JOIN 第三张表时,拿这个膨胀后的结果当主表,错误会级联放大
需要最新一条明细?用 ROW_NUMBER() 在 ON 子句里过滤
窗口函数是控制“取哪一条”的最精准手段,但必须注意 rn = 1 要写在 ON 里,不是 WHERE。
- 正确写法:
ON p.user_id = u.id AND p.rn = 1,能保留没日志的用户 - 错误写法:
WHERE p.rn = 1,会把没匹配到日志的用户全过滤掉(变相转成INNER JOIN) - PostgreSQL 支持
DISTINCT ON (user_id);MySQL 8.0+ 和 SQL Server 需用ROW_NUMBER();SQLite 3.25 以下不支持窗口函数,只能用相关子查询或临时表
只需要统计值?先 GROUP BY 再 JOIN
聚合必须发生在 JOIN 之前,否则中间结果已膨胀,SUM/COUNT 就失真。
- 对右表单独
GROUP BY user_id,生成带COUNT(*)、MAX(log_time)的中间集,再LEFT JOIN - 字段类型不一致会导致隐式转换失败,比如
users.id是INT,t_log.user_id是VARCHAR,JOIN可能退化为全表扫描甚至笛卡尔积 - MySQL 5.7+ 严格模式下,
SELECT u.id, u.name, COUNT(*) FROM users u JOIN t_log l GROUP BY u.id会报错,因为u.name未出现在GROUP BY或聚合函数中
根本不需要右表字段?改用 EXISTS 更安全
当你只关心“有没有关联记录”,而不是“关联了什么内容”,EXISTS 不仅逻辑清晰,还天然规避重复和 NULL 陷阱。
- 替代
LEFT JOIN ... WHERE l.id IS NULL:用NOT EXISTS (SELECT 1 FROM t_log l WHERE l.user_id = u.id) -
EXISTS找到第一条就终止扫描,比JOIN全量拉取再过滤快得多 - 不会受右表字段重复、
NULL值、索引失效等问题干扰 - 无法从
EXISTS中取右表字段——这不是缺点,而是提醒你:如果需要字段,说明业务目标本就不是“存在性判断”
最容易被忽略的一点:JOIN 后的结果结构由连接逻辑决定,不是由 SELECT 列表决定;哪怕你只选左表字段,重复仍会发生——因为 JOIN 是在投影前完成的。











