row_number() 和 rank() 执行计划无本质差异,性能取决于后续使用方式;row_number() 适用于取唯一代表行,rank() 适合需保留并列的排行榜场景,真实性能瓶颈常源于 partition by 列的数据倾斜。

ROW_NUMBER() 和 RANK() 在执行计划里其实没差别
两者底层都依赖窗口排序(Window Spool 或 Sort 算子),只要 ORDER BY 列相同、分区逻辑一致,SQL Server / PostgreSQL / MySQL 8.0+ 的执行计划几乎完全一样。性能差异不在函数本身,而在后续怎么用它们过滤或关联。
常见错误现象:WHERE ROW_NUMBER() OVER (...) = 1 直接写在 WHERE 子句里——会报错,因为窗口函数不能出现在 WHERE 中。必须用 CTE 或子查询包裹。
- 正确写法:用 CTE 先算号,再外层筛选
WHERE rn = 1 - 错误写法:
SELECT * FROM t WHERE ROW_NUMBER() OVER (PARTITION BY x ORDER BY y) = 1→ 语法错误 - 如果只取 Top 1,且不关心“并列怎么排”,
ROW_NUMBER()更可控;需要保留并列结果时才用RANK()
用 RANK() 导致重复数据膨胀的真实代价
RANK() 在遇到相同排序值时会跳过后续序号(比如两个第1名,下一个就是第3名),这本身不慢,但容易让人误以为“去重了”。实际它只是编号方式不同,原始行数一点没少——后续 JOIN 或聚合时,数据量可能翻倍甚至更多。
使用场景:报表里要展示“并列第1的选手有3个”,这时 RANK() 合理;但如果是找每个部门薪资最高的员工,用 RANK() 可能返回多个,而业务只要一个,就得额外加 MIN(id) 或 LIMIT 1 补救。
- MySQL 8.0+ 中,
RANK()和ROW_NUMBER()对索引利用程度一致,都依赖ORDER BY列上有合适索引 - PostgreSQL 中若
PARTITION BY列基数极高(比如每行都不同),RANK()的内存开销略高,因需缓存更多组内比较状态 - 避免在大表上对无索引字段(如
ORDER BY SUBSTRING(name, 1, 5))用任一函数——排序成本远大于编号逻辑
生产环境选哪个?看下游怎么消费结果
别纠结函数名字,盯紧你的 SQL 最后一步在干什么。
- 要取每个分组的“唯一代表行”(比如最新一条记录),无条件选
ROW_NUMBER(),配合WHERE rn = 1 - 要做排行榜展示(允许并列),且前端/BI 工具直接渲染序号列,用
RANK()更省事,不用自己处理重复逻辑 - 如果后续还要基于序号做范围筛选(如
WHERE rk BETWEEN 1 AND 10),RANK()可能漏掉实际第10名之后的并列者,而ROW_NUMBER()保证连续,更易预测结果集大小 - Oracle 用户注意:
DENSE_RANK()是更常被忽略的中间选项——并列不跳号,比RANK()更适合分页类场景
最常被忽略的性能放大器:PARTITION BY 的数据分布
真正拖慢两个函数的,从来不是 ROW_NUMBER() 还是 RANK(),而是 PARTITION BY 列的倾斜程度。比如按用户ID分区,但 5% 的用户占了 90% 的记录数,那么那个超大分区的排序就会卡住整个查询。
可观察执行计划里的 “Window Spool” 算子 I/O 和 CPU 占比,如果某一分区耗时占比远高于其行数占比,基本就是分布问题。
- 临时缓解:加
OPTION (MAXDOP 1)防止大分区被拆成低效并行任务(尤其 SQL Server) - 长期方案:对高频分区键预聚合,或改用
APPLY+TOP 1替代窗口函数(适用于单分组取 top 场景) - MySQL 用户注意:8.0 虽支持窗口函数,但对
PARTITION BY大量小分组(如百万级分区键)性能仍弱于 PostgreSQL
复杂点在于,你没法光看函数名判断快慢——得看数据长什么样,以及下一行 SQL 怎么用这个序号。










