推荐用offset-fetch实现分页,因其语义清晰、性能可控且原生支持;row_number()虽仍可用但写法冗长易错;top+子查询因漏行跳页且不直观已被淘汰。

SQL Server 2019 中推荐用 OFFSET-FETCH 实现分页,它语义清晰、性能可控,且原生支持;ROW_NUMBER() 嵌套子查询方式仍可用,但写法冗长、易出错,尤其在多排序字段或 NULL 处理时。
为什么不用 TOP + 子查询做分页?
早期常见写法如 SELECT TOP 10 * FROM t WHERE id NOT IN (SELECT TOP 20 id FROM t ORDER BY id) 在数据分布不均或主键不连续时会漏行、跳页;且无法表达“第 N 页,每页 M 条”的直观语义。SQL Server 2012 起已不鼓励这种模式,2019 更应避免。
OFFSET-FETCH 的正确写法和关键约束
必须配合 ORDER BY 使用,否则报错:The OFFSET clause is invalid unless a corresponding ORDER BY clause is specified.
-
OFFSET值从 0 开始:第 1 页 →OFFSET 0 ROWS,第 3 页(每页 20 条)→OFFSET 40 ROWS -
FETCH NEXT后必须跟具体数字和ROWS ONLY,不能省略ONLY - 排序字段建议加索引,否则
OFFSET 100000 ROWS会强制扫描前 10 万行,性能陡降 - 若需动态页码,用参数拼接时注意:页码 × 每页条数可能溢出
INT,建议用BIGINT计算OFFSET
示例(取第 5 页,每页 25 条,按创建时间倒序):
SELECT id, title, created_at FROM posts ORDER BY created_at DESC, id DESC OFFSET 100 ROWS FETCH NEXT 25 ROWS ONLY;
ROW_NUMBER() 方案还在什么情况下必须用?
当需要在分页结果中同时返回总记录数(COUNT(*) OVER())且不允许额外查一次表时,ROW_NUMBER() 是唯一选择——OFFSET-FETCH 本身不提供总数。
- 必须用子查询或 CTE 包裹,否则
ROW_NUMBER()和WHERE执行顺序会导致逻辑错误 - 排序字段含
NULL时,ORDER BY col DESC会让NULL排最前,而ORDER BY ISNULL(col, '1900-01-01') DESC可控,但OFFSET-FETCH同样受此影响 - 聚合后分页(如
GROUP BY)无法直接用OFFSET-FETCH,得先GROUP BY再套一层ROW_NUMBER()
示例(带总数的分页):
WITH paged AS (
SELECT id, title, COUNT(*) OVER() AS total_count,
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM posts
)
SELECT id, title, total_count
FROM paged
WHERE rn BETWEEN 101 AND 125;
真正要注意的是:OFFSET 越大,SQL Server 越要“跳过”前面所有行,哪怕你只想要最后 20 条——这没有银弹,如果业务真要深度翻页(比如 >1000 页),得考虑游标分页(WHERE created_at )或物化中间结果。











