说说SQL Server里两种分页方法到底差在哪儿,哪种更适合你用
- 问答
- 2026-01-04 20:49:54
- 26
关于SQL Server里两种分页方法的区别以及如何选择,这确实是一个老生常谈但又非常实际的问题,很多人可能只是知道有这两种方法,但并不清楚它们内在的差异,导致在实际应用中可能选择了不适合的方案,从而影响了系统性能,今天我们就来深入聊聊这个话题,主要参考了数据库领域常见的性能分析思路以及广大开发者的实践经验。
两种核心分页方法:ROW_NUMBER() 与 OFFSET-FETCH
第一种方法,也是SQL Server 2005版本之后被广泛使用了很多年的方法,是利用ROW_NUMBER()这个窗口函数,它的写法通常是这样的:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY 某排序列) AS RowNum
FROM 你的表
) AS T
WHERE T.RowNum BETWEEN 第N页开始的行 AND 第N页结束的行
就是先给查询结果按照某个顺序(比如按时间倒序)生成一个连续的行号,然后在外层查询中,通过指定行号的范围(比如第21到第40行)来截取特定页的数据。
第二种方法,是SQL Server 2012版本引入的,语法上更简洁的OFFSET-FETCH子句,它的写法是这样的:
SELECT * FROM 你的表 ORDER BY 某排序列 OFFSET 跳过的行数 ROWS FETCH NEXT 每页大小 ROWS ONLY
这个语法非常直观,OFFSET就是跳过前面多少条记录,FETCH NEXT就是接着取多少条记录,比如要取第二页(每页20条),就是OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLY。
表面相似,内核差异巨大
从结果上看,两种方法都能准确地返回你需要的哪一页数据,它们的执行方式和对性能的影响,尤其是在处理大数据量分页时,有着天壤之别,这正是“差在哪儿”的核心。
性能差异的关键:越往后翻页越慢的“坑”
这是两种方法最著名的区别,使用ROW_NUMBER()方法时,虽然你只取了某一页的数据,但数据库引擎在内部需要先为整个查询结果集生成行号,这个“整个查询结果集”是个关键点,如果你的基础查询(不加分页条件)会返回100万行数据,那么即使你只想要第1000页(比如只取20行),数据库也需要先为这100万行数据计算并分配好行号,然后再从这100万行中把你需要的20行筛选出来。
可以想象,当页码越来越靠后,需要跳过的行数(OFFSET)越来越大时,这个生成完整行号集再进行筛选的成本会越来越高,这就是为什么用ROW_NUMBER()分页时,用户会感觉翻到后面几十页、几百页时,速度明显变慢的原因,数据库做了大量的无用功。
而OFFSET-FETCH方法在理论上也存在类似问题,因为OFFSET的本质也是需要数据库知道跳过了多少行,在现代SQL Server的优化器下,尤其是当ORDER BY的列上有合适的索引时,它有可能采用更高效的执行计划,比如直接通过索引定位到开始位置,然后顺序读取FETCH指定的行数,从而避免处理整个结果集,但如果没有良好的索引支持,它同样会面临性能衰减的问题,普遍认为在索引优化得当的情况下,OFFSET-FETCH在后期的性能表现通常优于传统的ROW_NUMBER()方法。
索引利用的讲究
两种方法都极度依赖ORDER BY子句上的索引,如果没有索引,无论是哪种方法,数据库都可能需要进行全表扫描和一次昂贵的排序操作,性能会非常差。
但对于OFFSET-FETCH,一个设计良好的索引(特别是覆盖索引,即索引包含了查询中所有需要的列)能带来更大的性能提升,因为数据库可以完全在索引页上进行“跳过”和“读取”的操作,而不需要再回到原始的数据表(通常称为“键查找”或“书签查找”)去获取数据,这能极大减少I/O消耗。ROW_NUMBER()方法虽然也能利用索引,但其工作流程决定了它仍然可能需要处理大量中间数据。
灵活性对比
在灵活性上,ROW_NUMBER()其实更胜一筹,因为它是通过一个子查询先生成带行号的结果集,这个行号(RowNum)可以作为一个明确的字段在最终结果中呈现给前端,前端可以很方便地知道当前记录的总序号,这种子查询的结构也更容易嵌入更复杂的查询逻辑中。
而OFFSET-FETCH语法非常专注,就是做分页,它本身不提供额外的行号信息,输出结果就是纯粹的数据行,如果你需要总序号,还得另想办法。
哪种更适合你用?
现在我们来回答第二个问题,这完全取决于你的具体场景。
-
如果你的应用场景是“深度分页”很常见,或者数据量非常大:比如一个后台管理系统,用户经常需要翻到几十页甚至几百页之后去查找历史数据,你应该优先考虑使用
OFFSET-FETCH,并务必为ORDER BY的列建立高性能的索引,最好是覆盖索引,这是为了避免后期翻页时出现灾难性的性能下降,这是目前业界针对此类场景更推荐的现代化做法。 -
如果你的应用分页需求很浅,或者数据量本身就不大:比如一个新闻列表,用户绝大多数时候只看前两三页,总数据量可能就几千条,在这种情况下,两种方法的性能差异微乎其微,人眼根本无法感知,这时,你可以根据个人或团队的编码习惯来选择,甚至,你选择看起来更直观的
OFFSET-FETCH,或者觉得ROW_NUMBER()的写法更熟悉、更灵活,都是完全可以接受的。 -
一个更高级的选择:键集分页(Cursor-based Paging) 对于追求极致性能的超大数据集分页(例如社交媒体无限滚动),上面两种基于
OFFSET的方法都不是最佳选择,业界还有一种称为“键集分页”或“游标分页”的方法,它的原理不是记录“跳过多少行”,而是记录“上一页最后一条记录的位置”,查询时,直接定位到那个位置之后开始取数据。WHERE 排序列 > 上一页最后的值 ORDER BY 排序列 FETCH NEXT ...,这种方法无论翻到第几页,速度都一样快,因为它避免了OFFSET的扫描成本,但缺点是实现稍复杂,且不支持随机跳页(只能上一页、下一页)。
总结一下
简单总结,ROW_NUMBER()和OFFSET-FETCH的区别主要在于性能衰减曲线和索引利用效率。OFFSET-FETCH是现代SQL Server版本中更高效、更标准的分页写法,尤其在应对大数据量深度分页时,在索引的帮助下表现更好,而ROW_NUMBER()则提供了更多的灵活性。
对于新项目,建议直接习惯使用OFFSET-FETCH,而对于现有使用ROW_NUMBER()的系统,如果并没有遇到明显的性能问题,也不必急于修改,但如果已经出现了越翻越慢的情况,那么将分页逻辑重构为OFFSET-FETCH并优化相关索引,很可能是一剂良药,没有绝对的“最好”,只有针对你具体业务场景的“最合适”。

本文由颜泰平于2026-01-04发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/74543.html
