SQL 窗口函数怎么实现游标分页?

南乔
南乔
发布时间:2026-08-24| 149人浏览过
原创

前言

很多 SQL 教程会把“窗口函数编号”和“游标分页”混在一起,结果写出来的语句不是直接报错,就是翻页时重复、漏数。本文把这两个概念拆开讲清楚:先解释为什么 ROW_NUMBER() 不能直接承担游标分页,再给出正确的 ORDER BY + WHERE 写法、窗口函数能参与的辅助位置,以及不同数据库实现时该怎么判断边界是否可靠。

很多人第一次接触游标分页时,都会想到先用 ROW_NUMBER() 编号,再按编号截取下一页。但这条路在 SQL 里走不通:窗口函数解决的是“给结果打标记”,游标分页解决的是“从上一页最后一条继续往后取”。下面就按这个区别展开,先看为什么窗口函数不能直接做,再看正确写法、可用辅助场景,以及不同数据库落地时要注意的边界。

为什么窗口函数不能直接做游标分页

窗口函数本身不会改变结果集行数,ROW_NUMBER()、RANK() 这类函数只是为每一行附加一个计算值。它不能像游标分页那样,基于“上一页最后一条记录的状态”精确取得下一批数据。

说明窗口函数编号与游标分页边界定位差异的白底信息图
窗口函数与游标分页的职责边界把“编号截取”和“游标续取”拆开看,更容易理解为什么窗口函数不能直接承担游标分页。

游标分页的关键不是编号,而是有序偏移。也就是说,查询必须知道“从哪一条记录之后开始继续取”,而不是“全局第几行开始取”。

常见错误:直接在 WHERE 里过滤 rn

典型误写如下:

SELECT *, ROW_NUMBER() OVER (ORDER BY id) AS rn
FROM orders
WHERE rn > 100 AND rn <= 120;

这会报错:Window function is not allowed in WHERE clause。

原因在于 SQL 的逻辑执行顺序里,WHERE 先于窗口函数计算执行。写到 WHERE rn > 100 时,rn 这个别名还不存在,所以不只是思路不适合,语法层面也过不去。

即便把它包进子查询再过滤,得到的也只是“按编号截页”的效果,本质上仍然是位置分页思路,不是游标分页。

游标分页真正依赖什么

游标分页本质上是:基于上一页最后一条记录的排序字段值,查出下一批严格大于它的记录。这里真正起作用的是 ORDER BY 和 WHERE 的组合,而不是窗口函数。

说明正确游标分页 SQL 组成与边界处理的白底信息图
正确的游标分页由哪几部分组成真正稳定的游标分页,重点不在函数名,而在排序键、边界比较和客户端保存的游标值。

先有稳定排序,再谈游标

要让游标分页可靠,排序字段必须具备确定性。只用 created_at 往往不够,因为多条记录可能拥有相同时间;更稳妥的做法是使用 created_at + id 这样的组合键。

这样做的意义是,当时间相同的时候,仍然可以用 id 继续打破并列,避免翻页时出现重复或漏数据。

正确 SQL 写法是什么

推荐写法是基于行比较:

WHERE (created_at, id) > (?, ?)

这种写法表达的是一个完整的排序边界,适用于 PostgreSQL 和 MySQL 8.0+。

不要把它简化成下面这样:

WHERE created_at >= ? AND id > ?

这个条件看起来相似,实际在边界处理上是错的。它不能准确表达“先按 created_at 排,再按 id 排”的语义,容易在时间相同或边界交叉时造成漏数和重复。

首次请求与客户端游标保存

第一次请求没有游标时,可以使用 WHERE 1=1,或者直接省略 WHERE,但无论哪种方式都必须带上 LIMIT。

同时,客户端必须保存上一页最后一条记录的完整游标值,例如:

{"created_at": "2024-05-01 10:20:30", "id": 12345}

不能只保存单个字段。只存 created_at 会丢掉并列记录之间的相对位置,下一页就无法稳定续取。

窗口函数在游标分页里还能做什么

窗口函数不能承担分页本身,但它仍然能参与一些辅助场景。最典型的用途,是在返回当前页数据时,顺带判断“后面还有没有下一页”。

多取 1 条判断是否还有下一页

一种常见做法是:本来要查 20 条,实际先查 21 条。第 21 条如果存在,就说明还有下一页。

原始示例如下:

WITH page_plus_one AS (
SELECT *,
 ROW_NUMBER() OVER (ORDER BY created_at, id) AS rn
FROM orders
WHERE (created_at, id) > ('2024-05-01 10:20:30', 12345)
ORDER BY created_at, id
LIMIT 21
)
SELECT *,
 CASE WHEN rn = 21 THEN 'has_next' ELSE 'no_next' END AS pagination_hint
FROM page_plus_one
WHERE rn <= 20;

这里的关键点是:真正决定“下一页从哪开始取”的,仍然是内层的 WHERE (created_at, id) > (...) 和 ORDER BY created_at, id。ROW_NUMBER() 只是给这个临时结果集编号,方便外层判断是否超出当前页大小。

这个 CTE 写法为什么能成立

这段写法之所以可靠,是因为它先按游标条件和排序规则取出了一个有限结果集,再在这个结果集上做编号。也就是说,窗口函数参与的是“页内辅助判断”,不是“跨页定位”。

因此,这个模式必须和 ORDER BY、LIMIT 配合使用;如果缺少稳定排序,或者没有明确限制结果数量,那么 rn 的辅助意义就会变弱,分页提示也不够可靠。

不同数据库的实现差异与 NULL 处理

同样是游标分页,不同数据库对“组合字段比较”的支持并不完全一致。真正落地时,SQL 形式要跟着数据库能力走。

说明不同数据库游标分页支持差异与 NULL 排序影响的白底信息图
数据库支持差异与 NULL 边界处理数据库语法支持不同,NULL 排序规则也可能改变分页边界,这两点都需要在实现前先定清楚。

四类数据库的常见写法差异

PostgreSQL 的写法最直接,原生支持:

WHERE (a, b) > ($1, $2)

MySQL 8.0+ 也支持类似的行比较语法,但实践中要额外留意字符集和 NULL 的处理方式。

SQLite 则通常需要手动展开为:

WHERE a > ? OR (a = ? AND b > ?)

至于 SQL Server,它没有原生的行比较能力。常见替代方案是 OFFSET-FETCH,但这并不是游标分页,而是位置分页;当 offset 越来越大时,性能也会逐渐下降。

排序字段允许 NULL 时,规则必须先约定清楚

如果排序字段允许 NULL,问题会更复杂。不同数据库会把 NULL 排在最前或最后,默认行为并不完全一致,往往还受 NULLS FIRST/LAST 影响。

因此,游标分页里必须把排序规则写明确,例如:

ORDER BY created_at DESC NULLS LAST, id DESC

不仅 ORDER BY 要明确,WHERE 里的游标条件也要同步考虑 NULL 值。否则看起来排序一致,翻页时仍可能出现边界错位。

实操结论:判断一条 SQL 是不是游标分页,看这三点

如果一条 SQL 号称在做游标分页,可以直接检查三个问题。

  • 是否有稳定、确定性的排序键,例如 created_at + id
  • 是否基于上一页最后一条记录的排序值继续查询,例如 WHERE (created_at, id) > (?, ?)
  • 是否由客户端保存完整游标,而不是只保存页码或单个字段

只要核心逻辑还是“先编号,再按编号截取”,那它就不是游标分页。窗口函数在这里最多是辅助工具,真正决定分页准确性和性能表现的,始终是排序规则、边界条件和游标状态本身。

相关标签:

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

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 算法库。