row_number()分页更稳因按逻辑序号过滤,不依赖物理偏移,可避免重复排序值导致的漏行或重复;offset/fetch在created_at等非唯一字段排序时易出现翻页不一致,必须用order by+唯一列兜底。

ROW_NUMBER() 分页为什么比 OFFSET/FETCH 更稳
因为 ROW_NUMBER() 按逻辑序号过滤,不依赖物理偏移——遇到重复排序值(比如多个 created_at 相同的记录)时不会漏行或重复;OFFSET/FETCH 则可能跳过或重读某几条,翻页结果不可重现。
典型错误现象:ORDER BY created_at DESC 分页时,第 2 页和第 3 页出现相同记录,或某条记录始终查不到。
- 必须用
ORDER BY字段 + 唯一列兜底,例如ORDER BY created_at DESC, id DESC,否则窗口函数生成的序号不稳定 - 外层
WHERE必须写成BETWEEN @start AND @end形式,不能写成rn > @start AND rn ,否则优化器无法下推 TopN 优化 - 别名
AS rn不可省,否则外层引用报错Invalid column name 'rn'
深度分页(page_index > 10万)必须用 ROW_NUMBER()
OFFSET/FETCH 在深度分页时性能断崖式下跌,本质是引擎要“跳过 N 行物理数据”,B+ 树遍历开销线性增长;ROW_NUMBER() 虽然也排序,但能利用索引有序扫描 + StopAt 提前终止,实际只扫到目标范围就停。
常见错误:直接把 OFFSET (@page_index - 1) * @page_size 换成 ROW_NUMBER() 就以为万事大吉,却忽略索引支撑。
- ORDER BY 字段必须有高效索引,单字段不够时建组合索引,例如
CREATE INDEX ix_orders_time_id ON orders (created_at DESC, id DESC) - 如果内层查询带
WHERE条件(如status = 1),索引需覆盖该条件 + 排序字段,否则强制 Sort - 页码超大时(如
@page_index > 5000),建议主动拦截:IF @page_index > 5000 RAISERROR('Page too deep', 16, 1),避免拖垮 tempdb
参数计算与传参最容易踩的三个坑
所有分页方案都逃不开参数计算,但 ROW_NUMBER() 对溢出、越界、类型更敏感。
典型报错:Arithmetic overflow error converting expression to data type int,来自 (@page_index - 1) * @page_size 结果超过 2147483647。
- 用
DECLARE @offset BIGINT = CAST(@page_index AS BIGINT) - 1避免 int 溢出,再参与乘法 - 页码必须兜底:
DECLARE @page_index INT = ISNULL(NULLIF(@input_page, 0), 1),防传入 0 或负数导致rn BETWEEN 0 AND 9错误 -
@page_size建议硬限制 ≤ 100,防止恶意请求触发内存 grant 不足或排序溢出 tempdb
为什么加了索引还是慢?关键在执行计划里看两件事
即使写了标准的三层嵌套 ROW_NUMBER(),执行计划里没看到 Index Seek + Top,说明优化器没走索引有序扫描,而是退化成全表 Scan + Sort。
常见错误现象:5000 行就查 20 秒,加了 time 索引没效果,加了 status 索引也没效果。
- 检查内层子查询是否被优化器“折叠”:如果
WHERE status = 10和ORDER BY time DESC无法共用一个索引,就会先 Filter 再 Sort - 用
DBCC SHOW_STATISTICS确认统计信息是否过期,尤其大批量插入后未更新 - 避免在 ORDER BY 中用表达式(如
ORDER BY DATEADD(day, 1, created_at)),会强制 Sort
真正卡住的从来不是语法,是索引设计是否匹配查询模式,以及参数是否在边界上悄悄越界。










