OFFSET-FETCH 必须显式写 ORDER BY,OFFSET 从 0 开始,FETCH NEXT 后需跟数字+ROWS ONLY,排序字段含 NULL 时需用 ISNULL 控制;深分页性能差因强制扫描前 N 行,需覆盖索引缓解。

SQL Server 2019 中,OFFSET-FETCH 是默认推荐的分页方案,但“高效”不等于“无条件高效”——它在深分页(如第 10000 页)时性能会断崖式下降,必须配合索引和场景选择才能真正高效。
OFFSET-FETCH 必须满足哪些前提才不报错?
OFFSET-FETCH 不是拿来即用的语法糖,它有硬性约束:
- 必须显式写
ORDER BY,否则报错:The OFFSET clause is invalid unless a corresponding ORDER BY clause is specified. -
OFFSET值从 0 开始:第 1 页 →OFFSET 0 ROWS,第 5 页(每页 25 条)→OFFSET 100 ROWS -
FETCH NEXT后必须跟数字 +ROWS ONLY,漏掉ONLY会语法错误 - 排序字段若含
NULL,ORDER BY col DESC会让NULL排最前,影响分页一致性;可用ORDER BY ISNULL(col, '1900-01-01') DESC控制
为什么 OFFSET 越大越慢?怎么缓解?
OFFSET 50000 ROWS 不代表跳过 5 万行索引查找,而是强制 SQL Server 扫描并计数前 5 万行——哪怕你只取最后 20 条。这是它的底层行为,不是 bug。
- 唯一有效缓解方式:给
ORDER BY字段建**覆盖索引**,例如CREATE INDEX IX_posts_created_at_id ON posts(created_at DESC, id DESC) - 避免在无索引字段上分页,否则执行计划会出现
Sort操作,内存压力陡增 - 如果业务允许,把
OFFSET计算改为BIGINT类型,防止page * size溢出INT上限(2147483647)
什么时候必须改用 ROW_NUMBER()?
当你要在单次查询中同时拿到分页数据和总记录数,OFFSET-FETCH 就做不到——它不提供总数,而 ROW_NUMBER() 可以配合 COUNT(*) OVER() 实现。
- 必须用 CTE 或子查询包裹,否则
WHERE会在ROW_NUMBER()计算前执行,导致逻辑错误 - 聚合后分页(如
GROUP BY)无法直接套OFFSET-FETCH,得先聚合,再用ROW_NUMBER()包一层 - 多排序字段(如
ORDER BY status ASC, created_at DESC)时,ROW_NUMBER()的OVER子句写法更可控,OFFSET-FETCH容易因 NULL 处理不一致导致跨页重复或漏行
深分页(>1000 页)还有没有更快的路?
真要查第 50000 页,OFFSET-FETCH 和 ROW_NUMBER() 都会卡在 5 秒以上。这时候该换思路:用**游标分页(Keyset Pagination)**,基于上一页最后一条的排序键值做条件过滤。
- 示例:上一页最后一条是
created_at = '2024-01-15', id = 12345,下一页查:SELECT TOP 25 * FROM posts WHERE created_at 12345) ORDER BY created_at DESC, id DESC - 它不依赖偏移量,查询耗时恒定(约 3ms),但要求排序字段组合能唯一标识一行(主键或带唯一约束的列)
- 不能跳转任意页码,只支持“下一页/上一页”,适合无限滚动或翻页按钮场景
真正容易被忽略的点:分页性能不只取决于写法,更取决于索引是否匹配排序方向、是否覆盖查询字段、以及业务是否真的需要跳转到极深页码——很多所谓“第 10000 页”的需求,其实只是前端没做限制的副作用。

















