LEFT JOIN后主表数据“重复”是关系代数的必然结果,非SQL错误;根本原因是右表对同一关联键存在多条记录,导致主表行被复制;解决关键在于预聚合右表(如子查询GROUP BY)或用ROW_NUMBER()取单条,而非依赖DISTINCT。

LEFT JOIN后主表数据“重复”,不是SQL出错了,是它在严格按关系代数执行:只要右表对同一个关联键(如user_id)有多条记录,主表那行就会被复制多次。解决的关键不是“删重复”,而是控制右表参与JOIN的粒度。
为什么COUNT(*)突然翻倍?先用COUNT(DISTINCT)定位膨胀源
看到结果行数远超左表原始行数,别急着改JOIN写法。先确认是不是真膨胀、膨胀在哪:
- 执行
SELECT COUNT(*) FROM users和SELECT COUNT(*) FROM users u LEFT JOIN t_log l ON u.id = l.user_id,对比数值差异 - 再跑
SELECT COUNT(DISTINCT u.id) FROM users u LEFT JOIN t_log l ON u.id = l.user_id——如果这个值等于SELECT COUNT(*) FROM users,说明只是右表多行导致复制,主表本身没丢数据 - 用
SELECT u.id, COUNT(*) AS cnt FROM users u LEFT JOIN t_log l ON u.id = l.user_id GROUP BY u.id HAVING cnt > 1查出哪些主键被撑开了,方便后续针对性处理
需要右表聚合值(如最新IP、登录次数)?用子查询+GROUP BY预压平
这是最常用也最兼容的方案,适用于MySQL 5.7+、PostgreSQL、SQL Server等所有支持GROUP BY的引擎。核心是不让原始右表上场,而是先把它压成“一行一主键”:
- 子查询里必须
GROUP BY和ON条件完全一致的字段,比如ON是p.user_id = u.id,子查询就得GROUP BY user_id - 聚合函数按需选:
MAX(log_time)取最新时间,COUNT(*)统计次数,ANY_VALUE(ip_addr)取任意一条IP(MySQL 5.7+严格模式下,非分组字段必须套聚合函数) - 过滤条件(如
WHERE status = 'success')一定要写在子查询内部,而不是外层,否则无法减少中间数据量
SELECT u.id, u.name, p.cnt, p.latest_ip FROM users u LEFT JOIN ( SELECT user_id, COUNT(*) AS cnt, MAX(ip_addr) AS latest_ip FROM t_log WHERE log_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY user_id ) p ON p.user_id = u.id;
需要右表完整单条记录(如最新一条日志全部字段)?用ROW_NUMBER() + ON条件过滤
当你必须拿到ip_addr、log_time、user_agent等全部字段,且明确要“最新”或“最早”的那一条时,ROW_NUMBER()是唯一可控手段。注意关键细节:
-
AND p.rn = 1必须写在ON子句里,不能写进WHERE——否则会把没日志的用户全过滤掉,LEFT JOIN退化成INNER JOIN -
PARTITION BY user_id决定分组依据,ORDER BY log_time DESC决定排序方向;要最早一条就用ASC - MySQL 8.0+、PostgreSQL、SQL Server支持;MySQL 5.7及更早版本不支持窗口函数,只能退回用相关子查询或临时表
SELECT u.id, u.name, p.ip_addr, p.log_time, p.user_agent
FROM users u
LEFT JOIN (
SELECT user_id, ip_addr, log_time, user_agent,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY log_time DESC) AS rn
FROM t_log
) p ON p.user_id = u.id AND p.rn = 1;
DISTINCT为什么经常白加?它根本不解决逻辑膨胀
DISTINCT只对最终SELECT字段组合做整行去重。只要右表字段(如log_time、ip_addr)有一列不同,就不会合并——它不是按u.id去重,而是看整行是否完全一致。
- 如果你只查
u.id, u.name,DISTINCT能“看起来”去重,但代价是丢失所有右表信息 - 一旦你加上
l.log_time,重复立刻回来,因为每条日志时间都不同 -
DISTINCT底层常触发filesort和临时表,大数据量时比GROUP BY还慢 - 它掩盖问题:如果业务本该是一对一却出现一对多,
DISTINCT会让你错过数据一致性缺陷
最容易被忽略的一点:所有这些方案都要求右表关联字段类型严格一致。比如users.id是INT,而t_log.user_id是VARCHAR,JOIN可能退化为全表扫描甚至隐式转换失败——先检查DESCRIBE或EXPLAIN里的type和key列,确保走了索引。

















