RANK() 跳号是标准设计而非 bug,因其语义为“并列同名次且后续名次跳过被占位数”;需改用 DENSE_RANK() 消除跳号,且必须通过子查询或 CTE 在 WHERE 中过滤排名结果。

RANK() 的跳号不是 bug,是它明确设计的行为;要消除跳号,必须换函数,不能调参数或加条件。
为什么 RANK() 一定跳号?
RANK() 的语义就是“并列同名次,后续名次空出被占位数”。比如两行 score = 95 并列第 1,下一行哪怕只差 1 分,也得排第 3 —— 因为第 2 名已被逻辑上“预留”给不存在的中间名次。这不是计算错误,是标准定义(SQL:2003 起就固化了)。
常见错误现象:
- 写
RANK() OVER (ORDER BY score DESC)后发现名次序列是1,1,3,4,4,6,以为数据或排序错了 - 用该结果做
WHERE rk ,实际返回 4 行(因有两个 1 和一个 3),但业务预期是“最多 3 人”
DENSE_RANK() 是唯一语义匹配的替代方案
如果你要的是“并列第 1,下一个就是第 2”,DENSE_RANK() 就是为此存在的。它不预留名次,只按实际出现的不同值计数。
使用要点:
- 替换只需改函数名:
RANK()→DENSE_RANK(),其余语法完全一致 - 仍需
OVER (PARTITION BY ... ORDER BY ...),漏掉PARTITION BY会导致全表排名而非分组内排名 - 排序字段含 NULL 时,
NULLS LAST在 PostgreSQL/Oracle 有效,MySQL 8.0 不支持,得先用COALESCE(score, 0)处理
别在 WHERE 里直接用窗口函数过滤
WHERE dense_rank() OVER (...) 会报错,因为窗口函数执行时机晚于 WHERE。MySQL 8.0、PostgreSQL、SQL Server 全部拒绝这种写法。
正确做法只有两种:
- 套子查询:
SELECT * FROM (SELECT *, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) rnk FROM emp) t WHERE t.rnk - 用 CTE(更清晰):
WITH ranked AS (SELECT *, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) rnk FROM emp) SELECT * FROM ranked WHERE rnk
真正容易被忽略的点:排序键顺序决定“并列”的边界
写 ORDER BY sales DESC, hire_date ASC 时,并不是“先按 sales 排,再按 hire_date 补充”。而是:只有 sales 相同时,hire_date 才参与比较;一旦 sales 不同,hire_date 完全不影响当前行的名次判定。
这意味着:如果业务要求“同销售额时,入职早的优先”,那这个写法是对的;但如果误写成 ORDER BY hire_date ASC, sales DESC,就会变成“先按入职时间排”,导致华东区新员工销售额 99 万,可能排在华南区老员工 100 万之前——分组排名彻底失效。

















