row_number() 本身不直接去重,而是通过 partition by 分组 + order by 排序 + where rn = 1 筛选每组首行来实现“取每组第一条”;必须同时指定 partition by 和 order by,否则逻辑失效或结果不可控。

ROW_NUMBER() 分组去重的核心逻辑
直接说结论:ROW_NUMBER() 本身不“去重”,而是靠给每组内行编号 + 筛选序号为 1 的行,间接实现“取每组第一条”。关键不在函数,而在 PARTITION BY 和 WHERE 的配合。
写法必须包含 PARTITION BY,否则就不是分组
常见错误是只写 ORDER BY 忘了 PARTITION BY,结果变成全表编号,无法按业务维度(如用户、订单、日期)分组。比如要去掉重复的用户手机号,得按 user_id 或 phone 分组:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY phone ORDER BY created_at DESC) AS rn
FROM users
) t WHERE rn = 1;
-
PARTITION BY phone:把相同手机号的行归为一组 -
ORDER BY created_at DESC:组内按最新创建时间排,确保rn = 1拿到最新记录 - 没
PARTITION BY→ 全表只有一组 →rn就是 1,2,3…,完全达不到分组效果
ORDER BY 决定“留哪一条”,不写会报错或结果不可控
SQL 标准要求 ROW_NUMBER() 必须带 ORDER BY(部分旧版 MySQL 允许省略,但行为未定义)。实际中不明确排序规则,同一组内哪条是 rn = 1 完全随机,尤其在有重复 created_at 时更危险:
- 想留最新数据 →
ORDER BY updated_at DESC - 想留最早录入 →
ORDER BY id ASC(假设id自增) - 避免因时间字段为空导致排序不稳定 → 可追加
id作为次级排序:ORDER BY updated_at DESC, id DESC
和 DISTINCT、GROUP BY 的本质区别
DISTINCT 是去重后返回所有列的“唯一组合”,GROUP BY 必须配聚合函数;而 ROW_NUMBER() 方案能完整保留某一行的全部字段,且可自由控制“留哪一条”。但代价是性能开销更大,尤其大表没索引时:
- 如果只是去重查几个字段(如
SELECT DISTINCT name, email FROM t),别硬套ROW_NUMBER() - 若需保留整行 + 指定优先级(如“每个邮箱只留最新登录的那条完整记录”),这才是它不可替代的场景
- 确保
PARTITION BY和ORDER BY字段上有联合索引,例如(phone, created_at),否则OVER窗口计算可能变慢十倍以上
真正难的不是写对语法,而是想清楚“按什么分组”“依据什么留”“有没有隐含的空值或时区问题”。哪怕 PARTITION BY 写对了,ORDER BY 里漏了个 COALESCE(updated_at, '1970-01-01'),结果就可能出错。










