SQL Server 2012+ 存储过程使用 OFFSET/FETCH 分页必须满足三条件:强制 ORDER BY(否则报错)、排序字段有对应索引(避免全表扫描)、参数安全传入(防注入及溢出);深分页应改用游标分页或加缓存。

SQL Server 2012+ 存储过程中用 OFFSET 和 FETCH 分页是可行且推荐的,但必须满足三个硬性条件:有 ORDER BY、排序字段有索引、参数传入方式安全;漏掉任一条件,轻则结果错乱,重则报错或性能崩塌。
为什么存储过程里写 OFFSET 必须带 ORDER BY
这不是可选项,是 SQL Server 的语法强制要求。不加 ORDER BY 会直接报错:Invalid usage of the option NEXT in the FETCH statement。因为 OFFSET/FETCH 本质是“跳过前 N 行再取 M 行”,而“第几行”只有在确定顺序后才有意义。
-
ORDER BY字段必须有索引,否则即使语法通过,查询也会全表扫描——10 万行表查第 100 页可能要秒级响应 - 不能只写
ORDER BY id就完事;如果业务常按created_at DESC, id DESC查,索引就得建为CREATE INDEX IX_orders_sort ON orders(created_at DESC, id DESC) - 别用
SELECT *配合大宽表 + 深分页,IO 压力会集中在排序和跳过阶段,实际比ROW_NUMBER()还吃资源
OFFSET 和 FETCH 参数怎么安全传入存储过程
变量可以进 OFFSET 和 FETCH,但必须是参数化传入,严禁字符串拼接。否则用户传个 @page_size = '10; DROP TABLE users--' 就完了。
- 声明时加校验:
IF @page_size NOT BETWEEN 1 AND 100 THROW 50000, 'page_size must be 1-100', 1; - 计算
OFFSET要防溢出:DECLARE @offset INT = NULLIF(@page_index, 0); SET @offset = (@offset - 1) * @page_size;,再检查@offset 就直接退出 - SQL Server 不允许
FETCH NEXT @size ROWS ONLY中@size为负数或 NULL,运行时报错不是语法错误,而是执行期异常
遇到大偏移量(比如第 5000 页)响应慢,光改 SQL 没用
这是 B+ 树结构决定的物理限制:OFFSET 100000 不是“直接跳到第 10 万行”,而是让引擎从根节点开始逐层遍历、计数跳过——每跳一页都增加 IO 和 CPU 开销。此时优化 SQL 语法毫无意义。
- 真要支持深分页,得换游标分页(cursor-based pagination):记录上一页最后一条的
created_at和id,下一页查WHERE (created_at, id) - 游标分页必须配合联合索引,且不能跳页(不支持“跳到第 100 页”),但滚动加载场景下体验更稳
- 如果业务真需要任意页码跳转(如后台管理),建议加缓存层:把
COUNT(*)和常用页码结果预热到 Redis,避免每次查都扫表
最易被忽略的一点:很多人在存储过程里写了 OFFSET/FETCH,却忘了同步维护 COUNT(*) 总数逻辑——分页展示需要总页数,而 SELECT COUNT(*) FROM t WHERE ... 和主查询的 WHERE 条件必须完全一致,否则前台显示“共 200 页”,实际翻到第 199 页就空了。










