offset-fetch是sql server 2019推荐的分页方式,语法简洁、语义明确且执行计划干净,但强制要求order by、需扫描并丢弃前n行,页码过大时性能断崖式下降;游标分页更适合大数据量流式浏览。

OFFSET-FETCH 是 SQL Server 2019 中最直接、推荐的分页方式,但“高效”不等于“无条件快”——它只在合适条件下高效。盲目套用 OFFSET 查第 5 万页,性能会断崖式下跌。
为什么 OFFSET-FETCH 是首选,但不是万能解
它语法简洁、语义明确,SQL Server 优化器能较好识别执行意图;相比 ROW_NUMBER() 嵌套子查询,少一层 CTE 或派生表,计划更干净。但它强制要求 ORDER BY,且无法跳过前 N 行——OFFSET 100000 ROWS 就真得扫描并丢弃前 10 万行。
- 必须带
ORDER BY,否则报错:The OFFSET clause is invalid unless a corresponding ORDER BY clause is specified. -
OFFSET值从 0 开始:第 1 页 →OFFSET 0 ROWS,第 N 页(每页 M 条)→OFFSET (N-1)*M ROWS -
FETCH NEXT后必须跟数字 +ROWS ONLY,ONLY不可省略 - 参数计算时注意溢出:
(N-1)*M可能超出INT范围,建议用BIGINT类型变量或转换
当 OFFSET-FETCH 性能崩了,该换什么
测试显示:页码超过 1000 页后,OFFSET-FETCH 和 ROW_NUMBER() 耗时都升至百毫秒级;到 5 万页时双双破 5 秒,而游标分页(Keyset Pagination)稳定在 3 毫秒左右。
- 游标分页依赖上一页末条记录的排序键值,例如:
WHERE (OrderDate 12345) - 必须有覆盖索引支持排序字段组合,比如
IX_Orders_OrderDate_OrderId (OrderDate DESC, OrderId) - 不适用于“跳转到任意页码”的场景(如输入页码框),只适合“下一页/上一页”流式浏览
- 前端需保存并传递上一页最后一条的完整排序键,不能只传 ID
必须返回总记录数时,只能用 ROW_NUMBER()
OFFSET-FETCH 本身不提供总数,额外查一次 COUNT(*) 在高并发下会放大 I/O 和锁压力。此时唯一可行方案是用 ROW_NUMBER() + COUNT(*) OVER() 同步计算:
WITH paged AS (
SELECT id, title, created_at,
ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn,
COUNT(*) OVER() AS total_count
FROM posts
)
SELECT id, title, created_at, total_count
FROM paged
WHERE rn BETWEEN 101 AND 125;
- 必须用 CTE 或子查询包裹,否则
WHERE会先于窗口函数执行,导致逻辑错误 -
COUNT(*) OVER()不影响排序或过滤,但会强制全表扫描(除非有索引覆盖) - 若表有数十亿行且总数不常变,考虑用统计信息视图
sys.dm_db_partition_stats估算,避免实时计算
最容易被忽略的性能陷阱:排序字段含 NULL 或无索引
即使写了 ORDER BY created_at DESC,如果 created_at 列没索引,SQL Server 就得对全表排序;若该列大量为 NULL,默认 NULLS FIRST(SQL Server 行为),会导致分页结果顺序与预期不符,且索引失效。
- 建索引时显式包含所有
ORDER BY字段,顺序严格匹配,例如:CREATE INDEX IX_posts_created_id ON posts(created_at DESC, id DESC) - 处理
NULL:用ORDER BY ISNULL(created_at, '1900-01-01') DESC可控,但要同步在索引中体现该表达式(需计算列+索引) - 聚合后分页(如
GROUP BY)无法直连OFFSET-FETCH,必须先聚合再套ROW_NUMBER()










