JOIN后出现重复行是因一对多匹配导致左表行被复制,ROW_NUMBER()通过PARTITION BY分组编号、ORDER BY确定顺序、ON中筛选rn=1实现精准压缩,但需注意数据库版本兼容性及排序字段唯一性。

为什么JOIN后出现重复行?
连表时出现重复,往往是因为主表某条记录在从表中匹配到多条记录,比如一个订单对应多个商品、一个用户有多条登录日志。ROW_NUMBER() 本身不消除重复,它只是给每组内的行编号——关键在于「先编号,再筛选」,把多对一关系压缩成一对一。
用ROW_NUMBER() + 子查询过滤掉重复匹配
典型做法是:在子查询里对从表按关联字段分组编号,然后外层只取每组 ROW_NUMBER() = 1 的那行。注意排序逻辑必须明确,否则结果不可控。
示例(查每个用户的最新一次登录):
SELECT u.id, u.name, l.ip, l.login_time
FROM users u
LEFT JOIN (
SELECT user_id, ip, login_time,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time DESC) AS rn
FROM login_logs
) l ON u.id = l.user_id AND l.rn = 1;
-
PARTITION BY user_id确保每个用户独立编号 -
ORDER BY login_time DESC决定哪一行是“第1名”,这里取最新 -
AND l.rn = 1必须写在ON条件里,不能放WHERE,否则LEFT JOIN会退化成INNER JOIN
不同数据库对ROW_NUMBER()的支持差异
ROW_NUMBER() 在 PostgreSQL、SQL Server、Oracle、MySQL 8.0+ 都支持,但 MySQL 5.7 或更早版本不支持窗口函数,强行用会报错 ERROR 1064: This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' 类似提示——实际是语法不识别 OVER 子句。
替代方案(MySQL 5.7):
SELECT u.id, u.name, l.ip, l.login_time FROM users u LEFT JOIN login_logs l ON u.id = l.user_id LEFT JOIN login_logs l2 ON u.id = l2.user_id AND l2.login_time > l.login_time WHERE l2.user_id IS NULL;
这种自连接方式性能差、易出错,且无法处理时间相同的情况;升级到 MySQL 8.0+ 是更稳妥的选择。
容易被忽略的排序稳定性问题
如果 ORDER BY 字段存在重复值(比如多个日志时间戳完全一样),ROW_NUMBER() 仍会强制分配唯一序号,但每次执行顺序可能不同——这会导致“最新一条”结果不稳定。
解决方法:
- 补上唯一字段做二级排序,如
ORDER BY login_time DESC, id DESC - 避免用
datetime字段单独排序,尤其当写入精度为秒时 - 测试时加
ORDER BY外层显式排序,确认结果可复现
真正麻烦的不是写法,而是没意识到排序字段的业务语义是否足够唯一。

















