ORDER BY字段不唯一会导致ROW_NUMBER()行号分配不稳定,必须在排序末尾添加唯一列(如id)兜底;避免使用运行时函数或精度不足的时间字段;WHERE条件需用子查询或CTE套一层,且rn别名不可省略。

ORDER BY字段不唯一导致行号分配不稳定
ROW_NUMBER()本身不保证跨查询结果一致,只要ORDER BY里存在重复值(比如多个订单的created_at相同),数据库就可能在不同执行中以不同物理顺序排列这些行,导致同一行在不同请求里被分配不同序号。这不是bug,是SQL标准允许的未定义行为。
常见现象:第1页末尾出现id=105,第2页开头又出现id=105;刷新后某条记录“消失”或“跳页”。
- 必须在ORDER BY末尾加一个唯一列兜底,例如
ORDER BY created_at DESC, id DESC - 别依赖
getdate()、now()这类运行时函数参与排序——每次执行值不同,直接破坏稳定性 - 时间字段精度不够(如只到秒)在高并发写入下极易冲突,哪怕建了索引也救不了
WHERE条件写错位置,导致全表编号再过滤
很多人把WHERE rn BETWEEN 21 AND 40写在和ROW_NUMBER()同一层,但多数数据库(SQL Server、PostgreSQL)无法将这个条件下推到窗口计算前,结果是先对全表排序编号,再过滤,性能崩、内存爆、还容易因临时排序抖动引发重复。
- 务必用子查询或CTE套一层:
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t) t2 WHERE t2.rn BETWEEN ... -
rn别名不能省——漏写就会报Invalid column name 'rn' - 不要用
rn > 20 AND rn 替代<code>BETWEEN,语义等价但部分优化器识别不到TopN提示
索引没对齐OVER(ORDER BY),强制走Sort算子
即使你写了ORDER BY created_at DESC, id DESC,如果没建对应联合索引,数据库就得先全表扫描、再内存排序(Sort)、最后编号——I/O和tempdb压力陡增,深分页时延迟飙升,且排序过程本身就不稳定。
- 索引必须严格匹配OVER里的字段顺序和方向:
CREATE INDEX IX_posts_time_id ON posts(created_at DESC, id DESC) - 如果常带
WHERE status = 'published',把status放索引最左列:CREATE INDEX IX_posts_status_time_id ON posts(status, created_at DESC, id DESC) - 避免在排序字段上用表达式,比如
ORDER BY DATE(created_at)或ORDER BY UPPER(name)——索引直接失效
参数计算溢出或越界,让rn范围错位
用户传参@page_index = 0或@page_size = 1000时,(@page_index - 1) * @page_size可能算出负数或超int上限,导致rn BETWEEN -1 AND 99这种非法范围——有些数据库会静默修正,有些则返回错乱数据,甚至把第1页最后一条塞进第2页开头。
- 页码必须兜底:
DECLARE @page_index INT = ISNULL(NULLIF(@input_page, 0), 1) - 偏移量用BIGINT防溢出:
DECLARE @offset BIGINT = CAST(@page_index AS BIGINT) - 1 -
@page_size硬限制≤100,防止恶意请求拖垮排序内存
真正麻烦的不是语法写不对,而是没想清楚“你要求的‘不重复’到底依赖哪几个字段的全序关系”——时间戳只是业务语义,数据库只认确定性排序。

















