ROW_NUMBER 是什么
ROW_NUMBER() 是窗口函数,按分区内的排序给每一行生成连续且唯一的编号,从 1 开始,并列值也会被强行编成不同号码。它最常用于两类场景:分组取前 N 行,以及去重取最新一条。它不能直接写在 WHERE 里,必须套一层子查询或 CTE 之后再过滤。需要保留并列名次时改用 RANK 或 DENSE_RANK,需要严格 N 行时用 ROW_NUMBER。
基本信息
| 语法 | ROW_NUMBER() OVER (PARTITION BY 分组列 ORDER BY 排序列 ASC|DESC) |
|---|---|
| 编号特点 | 分区内连续唯一,并列值也按顺序编号 |
| 与 RANK 的区别 | RANK 并列跳号得 1,1,3;ROW_NUMBER 不跳号得 1,2,3 |
| 可用位置 | SELECT 列表与 ORDER BY,不能写在 WHERE |
| 省略 PARTITION BY | 整个结果集视为一个分区 |
| 版本要求 | MySQL 8.0+ 与 PostgreSQL 均支持 |
典型使用场景
- 每组取前 N:每部门薪资前三、每个类目销量前十
- 去重取最新:同一用户多条记录只保留最新一条
- 替代深分页:按编号切块拉取数据
- 数据抽样:每组取第一行作为样本做快速核对
- 生成明细序号:导出结果加连续行号便于逐行比对
- 任务切分:把编号取模把记录分给多个处理线程
每组取前 N
每组取前 N
SELECT * FROM (
SELECT dept_id, emp_name, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employees
) AS t
WHERE t.rn <= 3; # 每部门薪资前三去重取最新
去重取最新
SELECT * FROM (
SELECT u.*,
ROW_NUMBER() OVER (PARTITION BY user_id
ORDER BY created_at DESC, id DESC) AS rn
FROM user_logs AS u
) AS x
WHERE x.rn = 1; # 每个用户只保留最新一条
# 排序键要能唯一确定保留哪一行:created_at 相同时用 id 兜底编号函数对照
| 函数 | 并列值处理 | 结果示例 |
|---|---|---|
| ROW_NUMBER() | 继续顺序编号 | 1,2,3 |
| RANK() | 并列同名次并跳号 | 1,1,3 |
| DENSE_RANK() | 并列同名次不跳号 | 1,1,2 |
| NTILE(n) | 把分区均分成 n 桶 | 1 到 n |
| LAG / LEAD | 取前后行,不编号 | 上一行的值 |
窗口函数不能出现在 WHERE 里,必须在外层再过滤;MySQL 与 PostgreSQL 都是先算窗口再筛外层。
去重场景里如果排序键不唯一,保留哪一行由执行顺序决定,结果不可复现;一定要补上能唯一排序的键(通常再加主键)。
分区键与排序键上有合适索引时可以减少一次全量排序,但窗口函数通常仍会带来额外开销,数据量大时先用 EXPLAIN 观察。
取「每组前 N」时 ROW_NUMBER 严格返回 N 行,RANK 在存在并列时会返回超过 N 行,报表口径要求严格行数时选前者。
使用要点
- 外层过滤条件写在子查询外面,内层只负责编号
- 排序方向要与业务含义一致:要最新就 DESC,再补主键做次级键
- 大表上先用 WHERE 缩小范围再做编号,避免对全表开窗
- 编号结果可以复用:同一次开窗给多个函数共享 PARTITION BY 定义
- 需要保留全部并列时换 RANK,不要用 ROW_NUMBER 后再强拼条件
- 用 CTE 组织多层编号逻辑,比嵌套三层子查询可读得多
常见疑问
- 为什么不能写在 WHERE 里:窗口函数在 WHERE 之后才求值,必须先算再在外层过滤
- 并列值怎么处理:保留并列名次用 RANK,要求严格行数用 ROW_NUMBER
- 省略 PARTITION BY 会怎样:整个结果集视为一个分区,编号从 1 排到最后
- 编号顺序不稳定怎么办:排序键补主键做次级键,结果才能复现
- 能替代 LIMIT 分页吗:可以按编号区间切块拉取,避免大偏移扫描
- 多列分区怎么写:PARTITION BY 后接多列,用逗号分隔即可
把编号与业务键一起查出来便于排查:导出数据时同时保留 rn 与主键,发现异常行时可以直接定位到原始记录去核对。
编号只是附加的一列,不改变原有列:不想把 rn 暴露给前端时,把它留在内层查询即可,外层只选择需要的列。