首页/数据/SQL 速查/ROW_NUMBER 组内行号
聚合与窗口函数54 条

ROW_NUMBER 组内行号

为分区内的行按排序生成连续唯一编号(1,2,3…),用于分组取前 N 行、去重取最新等场景。

SQL 速查数据更新于 2026-10-13

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 暴露给前端时,把它留在内层查询即可,外层只选择需要的列。

易错点

  • 把 ROW_NUMBER 直接写进 WHERE,语句报错,误以为语法写错
  • 去重时排序键不唯一,保留的行每次执行可能不同,结果无法复现
  • 用 RANK 代替 ROW_NUMBER 做每组前 N,并列时返回行数超过 N
  • 在窗口排序键上用函数或表达式,无法利用索引且排序代价上升
  • 对超大表直接开窗编号,先做全量排序再过滤,内存与临时表压力大

数据来源与参考

  • MySQL 8.0 Reference Manual · ROW_NUMBER
  • PostgreSQL Documentation · Window Functions
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌