rank()和dense_rank()在相同值时结果不固定,根本原因是order by列存在重复且未指定决胜列,导致数据库按物理顺序等不确定因素排序;必须添加唯一非空列(如id)作为第二排序依据才能保证结果稳定。

为什么 RANK() 和 DENSE_RANK() 在相同值时结果不固定?
根本原因不是函数本身“随机”,而是当排序依据列(ORDER BY 子句中的列)存在重复值时,数据库没有明确指定「第二排序依据」,导致物理存储顺序、索引扫描路径或并行执行计划差异,让相同 SQL 多次运行返回不同行序。这不是 bug,是 SQL 标准允许的未定义行为。
典型现象:两次执行 SELECT id, score, RANK() OVER (ORDER BY score DESC) FROM scores;,score=85 的几条记录排名位置互换,甚至 ROW_NUMBER() 生成的序号也跳变。
必须加一个确定性排序列才能稳定排名
解决方法很简单:在 ORDER BY 中追加至少一个**唯一且非空**的列(如主键 id),作为决胜列(tie-breaker)。这样即使 score 相同,数据库也能按 id 稳定排序,整个窗口函数结果就可重现。
-
RANK() OVER (ORDER BY score DESC, id ASC)—— 相同 score 时按 id 升序排,排名并列但后续跳位 -
DENSE_RANK() OVER (ORDER BY score DESC, id ASC)—— 相同 score 时按 id 升序排,排名并列且不跳位 -
ROW_NUMBER() OVER (ORDER BY score DESC, id ASC)—— 强制全唯一序号,相同 score 内按 id 严格排序
注意:id 必须是 NOT NULL;如果用 created_at,要确认它无重复且精度足够(比如用 created_at, id 双重保险)。
用 ORDER BY ... NULLS LAST 防止 NULL 干扰排序稳定性
如果排序列可能为 NULL(比如 score 允许 NULL),默认排序行为因数据库而异(PostgreSQL 默认 NULLS LAST,Oracle 默认 NULLS FIRST),会导致跨库或升级后结果变化。显式声明可消除歧义:
RANK() OVER (ORDER BY score DESC NULLS LAST, id ASC)- MySQL 8.0+ 支持
NULLS FIRST/LAST;旧版 MySQL 或 SQLite 需用IF(score IS NULL, 1, 0), score DESC, id模拟 - SQL Server 不支持
NULLS语法,但ORDER BY score DESC, id默认将 NULL 排最前,若需排最后,得写成ORDER BY CASE WHEN score IS NULL THEN 1 ELSE 0 END, score DESC, id
别只靠索引,ORDER BY 才是真正起作用的地方
有人以为给 (score, id) 建联合索引就能让 RANK() 自动稳定——这是误解。索引只影响查询性能,不影响窗口函数的逻辑排序行为。哪怕有完美索引,只要 OVER (ORDER BY score DESC) 缺少决胜列,结果仍可能波动。
真正起决定作用的是 OVER 子句里的 ORDER BY 表达式本身。测试是否稳定,最直接方式是反复执行同一语句,观察输出是否字节级一致。
复杂点在于:业务上“相同值该不该并列”和“并列后怎么排”是两个决策,不能只甩给数据库默认行为;而那个看似多余的 , id ASC,往往是线上数据对账、分页、导出时不出错的关键一环。










