每组取前 N 行是什么
分组 Top N 的标准解法是先用窗口函数在组内编号,再在外层按编号过滤:ROW_NUMBER 严格返回 N 行,RANK 会保留并列名次因而可能多于 N 行,DENSE_RANK 不跳号。选择哪一个取决于业务对并列的处理:榜单严格限位用 ROW_NUMBER,允许并列用 RANK。窗口函数不能直接写在 WHERE 里,必须套一层子查询或 CTE,外层再按编号过滤,这是这一类查询的固定结构。
基本信息
| 基本结构 | 内层编号,外层按编号过滤 |
|---|---|
| ROW_NUMBER | 严格 N 行,并列也按顺序编号 |
| RANK | 带并列且跳号,可能多于 N 行 |
| DENSE_RANK | 带并列不跳号 |
| 分区与排序 | PARTITION BY 分组列,ORDER BY 排序列 |
| 版本要求 | MySQL 8.0+ 与 PostgreSQL 均支持窗口函数 |
典型使用场景
- 每部门薪资前三:人事与薪酬分析
- 每类目销量前十:电商榜单
- 每个用户最近一次登录:去重取最新
- 每组金额最大值所在行:取极值对应的完整记录
- 每组前百分之二十:用 NTILE 分桶后过滤
- 分页式取每组数据:按组内编号做游标切分
示例
示例
-- 每部门薪资前三:ROW_NUMBER 严格三行
SELECT * FROM (
SELECT dept_id, emp_name, salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE t.rn <= 3;
-- 允许并列:用 RANK,结果行数可能超过 3
SELECT * FROM (
SELECT dept_id, emp_name, salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
FROM employees
) t
WHERE t.rk <= 3;
-- 每组取金额最大的一条完整记录
SELECT * FROM (
SELECT o.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC, id DESC) AS rn
FROM orders o
) x WHERE x.rn = 1;编号函数选择
| 函数 | 行数结果 | 适用 |
|---|---|---|
| ROW_NUMBER | 严格等于 N | 榜单限位、去重取一条 |
| RANK | 可能大于 N | 允许并列的排名场景 |
| DENSE_RANK | 可能大于 N,名次连续 | 分段与档位划分 |
| NTILE(n) | 均分成 n 桶 | 按比例分组取前一批 |
外层过滤必须写在子查询之外:窗口函数在同一层不可用于 WHERE,这是语法层面的限制。
排序键要能唯一确定顺序,否则并列行的编号不稳定,每次执行结果可能不同;补上主键做次级键即可。
分区与排序键上有索引时可以减少一次排序,但窗口函数本身的开销仍存在,大表上先过滤再开窗。
使用要点
- 严格 N 行用 ROW_NUMBER,允许并列用 RANK
- 排序键补主键,保证编号结果可复现
- 先 WHERE 缩小范围再开窗,避免全表排序
- 需要从每组取完整记录时,外层用主键再回表关联
- 把编号逻辑放进 CTE,主查询只做过滤与展示
- 验证时分别检查每组行数与总数是否符合预期