子查询分页必须用 ROW_NUMBER(),需在子查询内写 WHERE 条件、显式命名子查询、ORDER BY 保证唯一性,并校验分页参数防溢出。

子查询分页必须用 ROW_NUMBER(),别想绕开它
SQL Server 2005 及以后版本支持 ROW_NUMBER(),这是子查询分页最稳定、最可控的方式。它不依赖主键连续性,也不怕重复值(只要 ORDER BY 字段组合能保证排序唯一),比 TOP NOT IN 或三重 TOP 嵌套更可靠。
常见错误是直接在子查询里 SELECT * 后加 ROW_NUMBER() 却没给子查询起别名,导致外部 WHERE 无法引用行号列;或者 ORDER BY 漏写、写错字段,使行号顺序混乱。
-
ROW_NUMBER()必须配合OVER (ORDER BY ...),且括号不能省略 - 子查询必须显式命名(如
AS t),否则外部无法WHERE t.RowNum BETWEEN ... - 排序字段建议包含主键或唯一索引列,避免因数据重复导致分页跳行或重复
- 如果表有
WHERE条件,务必放在子查询内部,而不是外部——否则会先全表编号再过滤,性能爆炸
OFFSET FETCH 是替代方案,但子查询分页不是它
很多人混淆“用子查询分页”和“用 OFFSET FETCH 分页”。后者是 SQL Server 2012+ 的原生语法,不依赖子查询,也不生成中间行号列。它写起来简洁,但有硬性限制:OFFSET FETCH 必须紧跟 ORDER BY,且不能用于视图、内联表函数或某些聚合场景。
如果你被要求“用子查询实现分页”,那 OFFSET FETCH 就不符合题意——即使它更快更干净。强行套用会导致逻辑错位,比如把 OFFSET 计算塞进子查询里,反而破坏语义清晰性。
-
OFFSET ((@PageIndex - 1) * @PageSize) ROWS FETCH NEXT @PageSize ROWS ONLY是独立语法,不是子查询 - 它不产生行号列,所以无法做“查第 N 页且同时返回总页数”这类需求(除非额外跑一次
COUNT(*)) - 在存储过程中拼接动态 SQL 时,
OFFSET FETCH更难调试,报错位置不如嵌套子查询直观
带条件的子查询分页:WHERE 要塞进内层,不是外层
真实业务几乎都有筛选条件,比如 “状态=已审核 且 创建时间 > ‘2025-01-01’”。这些条件若写在外层 WHERE,会导致子查询先对全表编号,再过滤——等于白算几万行的 ROW_NUMBER(),IO 和 CPU 都浪费。
正确做法是把所有业务条件都下推到子查询的 FROM ... WHERE 中,只让外层负责范围截取。
SELECT * FROM (
SELECT ROW_NUMBER() OVER (ORDER BY CreatedTime DESC, Id DESC) AS RowNum,
Id, Title, Status, CreatedTime
FROM Article
WHERE Status = 1 AND CreatedTime >= '2025-01-01' -- ✅ 条件在这里
) AS t
WHERE t.RowNum BETWEEN ((@PageIndex - 1) * @PageSize + 1) AND (@PageIndex * @PageSize)- 漏掉内层
WHERE是线上慢查询高频原因,尤其当表超百万行时,性能差异可达百倍 - 如果条件字段没索引,
ROW_NUMBER()排序成本会飙升;建议在ORDER BY字段和常用WHERE字段上建复合索引 - 不要为“兼容老版本”而放弃内层过滤——SQL Server 2005 支持
ROW_NUMBER(),也支持内层WHERE
参数计算容易溢出,BETWEEN 边界要防负数
@PageIndex 从 1 开始,但传入 0 或负数时,(@PageIndex - 1) * @PageSize 会变负,BETWEEN 左边界可能小于 1,导致无结果或报错(取决于 SQL Server 版本和 SET 选项)。生产环境必须校验参数。
- 建议在存储过程开头加判断:
IF @PageIndex -
@PageSize也要限制上限(如MAX(100)),防止恶意传极大值拖垮服务器 - 用
BETWEEN a AND b比RowNum >= a AND RowNum 更易读,但两者等价;注意 <code>a必须 ≤b,否则返回空 - 如果需要返回总记录数,别在同一条语句里硬塞
COUNT(*) OVER()——它会让执行计划退化;应单独查一次COUNT(*)缓存结果
真正卡住人的不是语法,是条件没下推、参数没校验、排序没索引——这三处一错,分页就从毫秒变秒级。

















