ROW_NUMBER()可实现分组内严格递增唯一序号,必须配合OVER(PARTITION BY...ORDER BY...)使用,缺一不可;ORDER BY需含确定性字段防结果不稳定,筛选“组内前N名”须用子查询或CTE。

用 ROW_NUMBER() 实现严格递增的组内序号
当需要为每个分组内的记录分配唯一、不重复且严格按顺序排列的编号(比如“第1名”“第2名”…),ROW_NUMBER() 是最直接的选择。它不关心值是否相同,只按 ORDER BY 子句排序后逐行累加。
常见错误是漏写 PARTITION BY,导致全表排序而非组内排序;或者把排序字段写成常量(如 ORDER BY 1),结果序号随机或报错。
-
PARTITION BY必须明确指定分组字段,例如用户ID、部门ID等 -
ORDER BY推荐使用确定性字段组合(如score DESC, create_time ASC),避免因排序不稳定导致每次查询结果不同 - 不能在
WHERE中直接引用窗口函数结果,需套一层子查询或 CTE
SELECT user_id, score,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY score DESC, id ASC) AS rank_in_dept
FROM users;
RANK() 和 DENSE_RANK() 的并列处理差异
遇到相同分数要并列排名时,选哪个函数取决于“跳名次”还是“不跳名次”。比如两个并列第1,则下一名是第3还是第2?
RANK() 会跳过被占掉的名次(1,1,3,4),DENSE_RANK() 则连续计数(1,1,2,3)。实际业务中,“前N名”筛选常用 RANK(),“梯队划分”倾向 DENSE_RANK()。
- 三者都要求
OVER子句完整,缺PARTITION BY或ORDER BY会报错 - 若排序字段含
NULL,不同数据库默认行为不同(PostgreSQL 默认NULLS LAST,MySQL 8.0 默认NULLS FIRST),建议显式声明 - 性能上无显著差异,但
ORDER BY字段未建索引时,大表可能明显变慢
SELECT dept_id, name, score,
RANK() OVER (PARTITION BY dept_id ORDER BY score DESC) AS rank_skip,
DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY score DESC) AS rank_dense
FROM employees;WHERE 条件里怎么过滤“组内前3名”?
窗口函数不能出现在 WHERE 中,这是初学者最常卡住的地方。必须把窗口计算放在子查询或 CTE 里,再在外层用 WHERE 筛选。
另一个易错点是误用 LIMIT 或 TOP —— 它们作用于最终结果集,不是每个分组。
- CTE 写法更清晰,适合多层嵌套逻辑;子查询适合简单场景
- 如果只需每个分组取1条,
ROW_NUMBER()+WHERE rn = 1比GROUP BY+ 聚合更安全(避免非确定性字段被隐式截断) - 注意别名作用域:子查询中定义的窗口别名,在外层才能被
WHERE引用
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn FROM products ) SELECT * FROM ranked WHERE rn <= 3;
MySQL 5.7 不支持窗口函数怎么办?
MySQL 5.7 及更早版本没有窗口函数,强行用会报错 ERROR 1064: You have an error in your SQL syntax。此时只能靠变量模拟,但要注意变量执行顺序不保证,尤其在 ORDER BY 和 PARTITION BY 共存时极易出错。
真正稳妥的做法是升级到 MySQL 8.0+,或改用应用层分组排序。临时方案中,用自连接实现 RANK() 类逻辑虽可行,但 N² 复杂度在万级数据上就会明显拖慢。
- 变量方式仅限单线程、小数据、测试环境应急,生产环境不要依赖
- 如果用的是 MariaDB,10.2+ 已支持标准窗口函数,检查版本比硬写变量更值得优先做
- 某些 ORM(如 Django 3.2+、SQLAlchemy 1.4+)已封装窗口函数语法,但底层仍依赖数据库能力,不能绕过版本限制
窗口函数不是语法糖,它改变了 SQL 的执行模型——从“逐行处理”变成“先分组、再跨行计算”。理解这点,才能避开那些看似合理却总查不出数据的陷阱。

















