根本原因是order by字段不唯一导致row_number()序号漂移,同一行可能跨页重复出现;必须用子查询/cte套用、建立匹配索引、避免表达式、防止参数溢出并用唯一列兜底定序。

根本原因不是语法写错了,而是 ORDER BY 字段不唯一,导致数据库每次执行时对相同值的行分配不同物理顺序,ROW_NUMBER() 生成的序号随之漂移——同一行在第1页和第2页都可能出现。
ORDER BY 字段重复时,ROW_NUMBER() 的序号不固定
只要 ORDER BY 中存在重复值(比如多个订单的 created_at = '2026-08-10 14:30:00'),数据库就无法保证这些行之间的相对顺序。这不是 bug,是 SQL 标准允许的未定义行为。
- 第一次查询可能把
id = 105排在同时间组的第1位,分配rn = 101 - 第二次查询它被排到第3位,变成
rn = 103,于是出现在下一页 - 你看到的现象:第1页末尾有
id=105,第2页开头又有id=105
WHERE 条件写错位置,导致全表编号再过滤
很多人直接在窗口函数同层写 WHERE rn BETWEEN 21 AND 40,但多数数据库(SQL Server、PostgreSQL)无法将该条件下推到窗口计算前,结果是先对全表排序编号,再过滤——性能差、内存爆、且排序过程本身就不稳定。
- 必须用子查询或 CTE 套一层:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn FROM orders ) t WHERE t.rn BETWEEN 21 AND 40
-
rn别名不能省,漏写会报Invalid column name 'rn' - 别用
rn > 20 AND rn 替代 <code>BETWEEN,部分优化器识别不到 TopN 提示
索引没对齐 ORDER BY,强制走 Sort 算子
即使你写了 ORDER BY created_at DESC, id DESC,如果没建对应联合索引,数据库就得全表扫描 + 内存排序(Sort),I/O 和 tempdb 压力陡增,深分页时延迟飙升,且排序过程本身就不稳定。
- 索引必须严格匹配
OVER里的字段顺序和方向:CREATE INDEX IX_orders_time_id ON orders(created_at DESC, id DESC) - 如果常带
WHERE status = 'paid',把status放索引最左列:CREATE INDEX IX_orders_status_time_id ON orders(status, created_at DESC, id DESC) - 避免在排序字段上用表达式,比如
ORDER BY DATE(created_at)或UPPER(name)——索引直接失效
参数计算溢出或越界,让 rn 范围错位
用户传参 @page_index = 0 或 @page_size = 1000 时,(@page_index - 1) * @page_size 可能算出负数或超 int 上限,导致 rn BETWEEN -1 AND 99 这种非法范围。
- 用
DECLARE @offset BIGINT = CAST(@page_index AS BIGINT) - 1避免 int 溢出 - 页码兜底:
DECLARE @page_index INT = ISNULL(NULLIF(@input_page, 0), 1) -
@page_size建议硬限制 ≤ 100,防止恶意请求触发内存 grant 不足或排序溢出 tempdb
最容易被忽略的一点:你以为加了 ORDER BY 就万事大吉,其实它只是“排序”,不是“定序”——只有加上唯一列兜底,才真正让每一行在逻辑序列中有确定位置。











