sql server存储过程分页优先用offset fetch(2012+),但必须带order by且排序字段有对应索引;row_number()兼容老版本,但动态拼接易致sql注入和性能问题。

SQL Server 存储过程中分页,优先用 OFFSET FETCH(2012+),但必须带 ORDER BY 且排序字段要有索引;ROW_NUMBER() 更兼容老版本,但动态拼接时易出 SQL 注入和性能问题。
为什么 OFFSET FETCH 必须配 ORDER BY
这不是建议,是硬性语法限制。不写 ORDER BY 直接报错:Invalid usage of the option NEXT in the FETCH statement。因为 OFFSET 100 ROWS FETCH NEXT 20 ROWS ONLY 的语义依赖“确定顺序”——没有顺序,“第101行”就无定义。
常见错误现象:
- 把
ORDER BY (SELECT NULL)当万能解,结果每次执行顺序随机,分页错乱 - 只按
id排序,但业务实际查的是created_at DESC,导致前端翻页跳数据
正确做法:
- 排序字段必须与业务查询逻辑一致,比如前端按时间倒序加载,就写
ORDER BY created_at DESC, id DESC - 该组合字段必须有对应索引,例如
CREATE INDEX IX_logs_time_id ON logs(created_at DESC, id DESC)
ROW_NUMBER() 分页在存储过程里怎么防注入和溢出
用 ROW_NUMBER() 常见于兼容 2005–2012 的场景,但多数人直接字符串拼接表名、条件、排序,埋下严重隐患。
关键风险点:
-
@TableName或@OrderBy若未经QUOTENAME()处理,传入'; DROP TABLE users--就炸了 -
ORDER BY (SELECT NULL)虽能绕过排序要求,但结果不可预测,且无法走索引,深分页极慢 -
@StartRow计算没校验:若@PageIndex = 0,(@PageIndex - 1) * @PageSize得负数,BETWEEN -10 AND 0返回空或报错
实操建议:
- 表名/列名一律用
QUOTENAME(@TableName)、QUOTENAME(@SortColumn) - 排序字段不允许为空,强制传参,拒绝
ORDER BY (SELECT NULL) - 计算前加判断:
IF @PageIndex
深分页(如第5000页)为什么 OFFSET 越来越慢
不是 SQL 写得不好,是 B+ 树物理结构决定的:引擎必须从根节点开始逐层遍历、计数跳过前 N 行。OFFSET 100000 不是“跳转”,是“计数跳过”,IO 和 CPU 成线性增长。
典型表现:
- 第1页 20ms,第100页 300ms,第5000页直接超时
- 执行计划里出现大量“Index Scan”或“Table Scan”,即使有索引也扫全表
此时换方案比调优 SQL 更有效:
- 改用游标分页:记录上一页最后一条的
created_at和id,下一页查WHERE (created_at, id) - 游标分页必须配合联合索引,且不能跳页(不支持“跳到第100页”),但滚动加载场景下稳定可靠
- 对必须跳页的管理后台,加缓存总条数 + 前几页数据,或前端禁用大页码输入
OFFSET FETCH 和 ROW_NUMBER() 性能对比的关键差异
两者在中小偏移量(
-
OFFSET FETCH是声明式语法,SQL Server 优化器能更好估算代价,执行计划更干净;但参数必须为非负整数,NULL或负值运行时报错,不是编译期报错 -
ROW_NUMBER()是窗口函数,需先生成完整编号再过滤,内存压力略高;但允许更灵活的子查询嵌套,适合复杂条件组装 - 都依赖排序字段索引——没索引时,
ROW_NUMBER()可能走Sort算子撑爆 tempdb,OFFSET FETCH则直接全表扫描
容易被忽略的一点:无论用哪种,SELECT * 配合宽表 + 深分页,IO 压力会集中在排序和跳过阶段,比只查必要字段慢数倍。别省那几行字段声明。










