SQL 用 ROW_NUMBER() 分页为什么会重复数据?

方予
方予
发布时间:2026-08-22| 162人浏览过
原创

前言

用 ROW_NUMBER() 做分页时,很多人第一反应是检查语法,但真正导致“上一页最后一条又出现在下一页”的,往往是排序不稳定、过滤层级不对,或者索引与参数边界没有收紧。本文把这几个高频误区拆开说明,并给出可直接落地的 SQL 与索引写法,帮助你同时判断结果为什么会重复,以及执行计划为什么会越来越慢。

用 ROW_NUMBER() 做分页时,最让人困惑的问题往往不是查不出来,而是“上一页最后一条,下一页又出现一次”。这类重复通常不是数据库出错,也不只是 SQL 写法细节,而是分页逻辑里少了“稳定定序”这一层约束。下面按结果稳定性、查询写法、索引匹配和参数边界四个方面拆开讲,帮助你判断问题到底出在哪一层。

为什么 ROW_NUMBER() 分页会出现重复数据

根本原因通常不是 ROW_NUMBER() 本身,而是 ORDER BY 字段不唯一。只要排序字段里存在重复值,数据库每次执行时就可能为这些“并列行”分配不同的物理顺序,最终让同一条记录的 rn 发生漂移。

展示 ORDER BY 不唯一时同一条记录在两次查询中 rn 漂移,导致跨页重复出现的白底信息图
序号漂移如何造成跨页重复当排序键存在重复值时,ROW_NUMBER() 只是在当前执行结果上编号。

例如多个订单的 created_at = '2026-08-10 14:30:00',数据库只能保证它们按 created_at 这个值参与排序,却不保证这些相同时间的记录彼此之间谁先谁后。这不是 bug,而是 SQL 标准允许的未定义行为。

  • 第一次查询时,可能把 id = 105 排在同时间组第 1 位,分配 rn = 101
  • 第二次查询时,它又可能被排到第 3 位,变成 rn = 103
  • 最终现象就是:第 1 页末尾出现 id=105,第 2 页开头又出现 id=105

所以要先明确一点:ORDER BY 只能完成“排序”,不一定能完成“定序”。只有在排序条件最后补上唯一列兜底,例如 id,每一行在逻辑序列里才有确定位置。

先解决排序不稳定:给 ORDER BY 加唯一列兜底

如果当前分页 SQL 只写了类似下面这种排序:

ORDER BY created_at DESC

那么在 created_at 重复的情况下,分页结果天然不稳定。更稳妥的做法是把唯一列追加到排序尾部,形成完整定序:

ORDER BY created_at DESC, id DESC

这样处理后,即使多条记录的 created_at 完全相同,数据库也可以继续用 id 决定先后顺序,避免 ROW_NUMBER() 在多次执行之间漂移。

这一步看起来只是多写一个字段,实际决定的是分页结果是否可复现。只要业务允许,兜底列就应该选真正唯一且稳定的字段,而不是另一个也可能重复的业务字段。

正确写法:先编号,再在外层按 rn 取页

另一个常见问题,是把 rn 的过滤条件放在错误层级。很多人会直接在窗口函数同层写 WHERE rn BETWEEN 21 AND 40,但在 SQL Server、PostgreSQL 这类数据库里,优化器通常不能把这个条件下推到窗口计算之前。

结果就是先对全表排序并编号,再从结果里过滤第 21 到 40 行。这样不仅性能差,还会把排序成本和内存压力放大,深分页时尤其明显。

更稳妥的写法是先用子查询或 CTE 生成 rn,再在外层过滤:

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'
  • 优先使用 BETWEEN 21 AND 40 这种区间表达,别随手改成 rn > 20 AND rn <= 40
  • 部分优化器对 BETWEEN 形式更容易识别 TopN 类优化意图

这里的重点不是语法能不能跑通,而是让“编号”和“取页”成为两个清晰阶段。这样更容易保证结果语义稳定,也更方便你继续检查执行计划。

索引必须和 OVER(ORDER BY ...) 对齐

即使你已经把排序写成了 ORDER BY created_at DESC, id DESC,如果没有匹配的联合索引,数据库仍可能走全表扫描,再额外执行内存排序 Sort。这会直接推高 I/O、内存 grant 和 tempdb 压力,页数越深越明显。

展示正确的 ROW_NUMBER 分页写法、索引对齐要求与常见失配点的白底信息图
正确分页结构与索引匹配关系分页是否稳定,不只取决于 SQL 能否执行,还取决于编号层级是否正确,以及索引能否直接支持排序。

索引设计要严格跟随窗口函数里的排序字段、顺序和方向:

CREATE INDEX IX_orders_time_id ON orders(created_at DESC, id DESC)

如果查询经常还会带上 WHERE status = 'paid',则应把过滤列放在最左侧:

CREATE INDEX IX_orders_status_time_id ON orders(status, created_at DESC, id DESC)

三个常见失配点

  • 索引字段顺序和 OVER 中的排序顺序不一致
  • 排序方向不一致,例如索引是升序、分页却按降序取值
  • 在排序字段上套表达式,导致索引直接失效

尤其要避免这类写法:

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 的非法或异常范围,结果自然会出现偏移。

可以直接按下面方式做边界收口:

DECLARE @offset BIGINT = CAST(@page_index AS BIGINT) - 1
DECLARE @page_index INT = ISNULL(NULLIF(@input_page, 0), 1)
  • 用 BIGINT 参与偏移量计算,避免 int 溢出
  • 把非法页码兜底到第 1 页,避免出现 0 页、负页
  • @page_size 建议硬限制 <= 100,防止恶意请求触发内存 grant 不足或排序溢出 tempdb

这部分容易被忽视,因为 SQL 本身看起来是对的,但参数一旦越界,分页区间就已经失真,最终表现出来也可能是“为什么这一页和上一页对不上”。

排查这类问题时,重点看这四件事

如果你正在处理线上分页重复、跨页重叠或深分页变慢,可以按下面顺序快速核对:

  1. 检查 ORDER BY 是否以唯一列兜底,例如 created_at DESC, id DESC
  2. 确认 ROW_NUMBER() 放在子查询或 CTE 中,外层再按 rn 过滤
  3. 确认联合索引是否与 OVER(ORDER BY ...) 的字段、顺序、方向一致
  4. 检查 @page_index、@page_size 的边界和偏移量计算是否存在溢出或越界

最关键的判断标准只有一个:你的分页语句是否真的为每一行建立了稳定、唯一、可复现的逻辑顺序。只要这一步没做完整,哪怕 SQL 能跑、索引也存在,ROW_NUMBER() 分页依然可能出现重复数据。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。