left join重复是语义必然结果,非bug;需据需求选择预聚合(group by)、窗口函数(row_number)或exists,而非盲目用distinct。

重复数据不是 SQL 的 bug,而是 JOIN 语义的必然结果;解决它不能靠“加个 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)都基于这个“膨胀后”的结构——这点不厘清,再怎么调 DISTINCT 或 GROUP BY 都只是在补漏。











