OFFSET/FETCH 仅适合中小规模分页(如前100页、每页20–100条)且ORDER BY字段有高效索引的场景;深度分页(page_index > 10万)时ROW_NUMBER()更稳更快,因其支持索引有序扫描与提前终止,避免OFFSET的物理偏移式遍历开销。

SQL Server 分页别无脑选 OFFSET/FETCH,尤其当 page_index > 10万时,ROW_NUMBER() 往往更稳、更快。
OFFSET/FETCH 在什么场景下真香?
它语法干净、语义清晰,适合中小规模分页(比如前 100 页,每页 20–100 条),且 ORDER BY 字段有高效索引时效果最佳。
- 必须带
ORDER BY,否则直接报错:Invalid usage of the option NEXT in the FETCH statement -
OFFSET (@page_index - 1) * @page_size ROWS中的乘法结果不能溢出 int 范围(SQL Server 里 int 最大是 2147483647) - 若
@page_size过大(如 > 500),即使页码小,也可能触发内存 grant 不足或排序溢出 tempdb - SQL Server 2012+ 才支持;旧版本强行用会语法报错,不是兼容性警告
ROW_NUMBER() 为什么在深度分页反而更抗压?
它靠窗口函数生成序号后过滤,虽然逻辑上要扫更多行,但引擎能利用索引有序扫描 + 提前终止(StopAt),避免 OFFSET 那种“跳过 N 页物理页”的 B+ 树遍历开销。
- 典型写法:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id DESC) AS rn FROM orders) t WHERE t.rn BETWEEN 6280001 AND 6280300 - 关键点:外层
WHERE的范围必须能被优化器识别为 Seek + Top,否则会全表扫 - 如果
ORDER BY字段没索引,ROW_NUMBER()会强制 Sort,性能雪崩——这点和 OFFSET 完全一致 - 注意别漏掉别名
AS rn,否则外层引用t.rn报错Invalid column name 'rn'
参数化传参时最容易栽的两个坑
无论用哪种方式,存储过程中传页码都得防注入、防越界、防计算溢出。
- 必须用
DECLARE @page_index INT = ISNULL(NULLIF(@input_page, 0), 1)强制兜底,避免传入 0 或负数导致OFFSET -100 ROWS报错 -
@page_size建议硬限制 ≤ 100,防止恶意请求FETCH NEXT 10000 ROWS拖垮连接池 - 计算偏移量时别写成
@page_index * @page_size,正确是(@page_index - 1) * @page_size;否则第 1 页就跳过前@page_size行 - 对超大偏移量(如 > 500000)可加判断:
IF (@page_index > 5000) RAISERROR('Page too deep', 16, 1),主动拒绝而非硬扛
执行计划里一眼看出谁在拖慢你
打开 SET STATISTICS XML ON 看实际执行计划,重点关注三个指标:
-
EstimatedRows和ActualRows是否严重偏离?偏离越大,统计信息越可能过期,先UPDATE STATISTICS - 看是否有
Sort算子占 CPU 主导——说明ORDER BY字段缺索引,两种写法都会崩 - OFFSET 方案里若出现高占比的
Index Scan或Key Lookup,基本等于宣告失效;ROW_NUMBER 方案里若外层Filter后只剩几行,说明 Seek 有效 - 留意
Warnings栏:出现Unmatched index或Column with no statistics就别纠结写法,先建索引
真正卡住性能的往往不是语法选型,而是 ORDER BY 字段有没有覆盖索引、统计信息是否陈旧、以及 page_index 是否已经大到让 B+ 树遍历变成 IO 密集型操作——这些比 OFFSET 和 ROW_NUMBER() 的微小差异重要得多。










