应优先使用offset/fetch而非row_number(),因其更轻量、执行计划更优;动态sql须用quotename+sp_executesql防注入;大数据量时改用键集分页提升性能。

SQL Server 里写分页存储过程,别一上来就套 ROW_NUMBER() + BETWEEN,尤其当页码很大(比如第1000页)时,性能会断崖式下跌——它得先给全表打序号,再筛范围,IO和CPU都扛不住。
用 OFFSET / FETCH 而不是 ROW_NUMBER() 做主查询
SQL Server 2012+ 支持原生分页语法,比嵌套 ROW_NUMBER() 更轻量、更易读、执行计划也更干净。
-
OFFSET直接跳过前 N 行,不生成中间序号列,避免大表排序开销 - 必须搭配
ORDER BY使用,否则报错:The ORDER BY clause is required for the OFFSET and FETCH clauses. - 不能和
TOP混用;若需返回总条数,得另起一次COUNT(*)查询(或用COUNT(*) OVER(),但注意它会阻止流式执行)
示例:
CREATE PROCEDURE GetPagedOrders
@PageNumber INT = 1,
@PageSize INT = 20
AS
BEGIN
SET NOCOUNT ON;
SELECT OrderID, CustomerID, OrderDate
FROM Orders
ORDER BY OrderID
OFFSET (@PageNumber - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;
END
动态表名和 WHERE 条件必须用 QUOTENAME + sp_executesql
如果存储过程要支持任意表、任意条件(比如后台通用列表),硬拼字符串等于敞开 SQL 注入大门。光靠 REPLACE(@input, '''', '''''') 不够,QUOTENAME 才是防注入的底线。
-
QUOTENAME(@TableName)会把Users; DROP TABLE Users--变成[Users; DROP TABLE Users--],直接让非法字符失效 -
WHERE条件必须作为参数传给sp_executesql,不能拼进 SQL 字符串里,否则参数化失效 - 别在动态 SQL 里写
ORDER BY (SELECT NULL)—— 它会让索引失效,应强制要求调用方传排序字段
错误示范:EXEC('SELECT * FROM ' + @TableName + ' WHERE ' + @WhereClause)
正确写法片段:
DECLARE @sql NVARCHAR(MAX) = N'
SELECT * FROM ' + QUOTENAME(@TableName) + N'
WHERE 1=1 ' + @WhereClause + N'
ORDER BY ' + QUOTENAME(@OrderByColumn) + N'
OFFSET @Offset ROWS FETCH NEXT @Fetch ROWS ONLY';
<p>EXEC sp_executesql @sql,
N'@Offset INT, @Fetch INT',
@Offset = (@PageNumber-1)*@PageSize,
@Fetch = @PageSize;</p>
大数据量下避免 OFFSET 大偏移的替代方案
当 @PageNumber 达到上千、@PageSize 为 50 时,OFFSET 49950 ROWS 仍需扫描前 5 万行——这时即使有索引,I/O 也吃紧。真要撑住,得换思路。
- 用「键集分页」:记住上一页最后一条记录的排序键(如
OrderID = 12345),下次查WHERE OrderID > 12345,配合TOP @PageSize - 该方式不支持跳页(比如从第1页直接跳第100页),但翻页响应快、并发友好
- 必须确保排序字段有唯一性或加组合唯一约束,否则可能漏/重数据;若用非唯一字段(如
CreateTime),需补上主键做第二排序 - 不要用
NOT IN或NOT EXISTS模拟 offset——它们在重复值多时逻辑易错,且优化器常选不到好执行计划
真正难的不是写出能跑的分页过程,而是判断什么时候该用 OFFSET/FETCH,什么时候该切到键集分页,以及怎么让调用方安全地传入排序字段和条件——这些细节漏掉一个,线上查慢、查错、被注入,都是分分钟的事。











