Codex写分页接口为什么越翻越慢?用Cursor Pagination解决重复与漏数据

使用 Codex 编写列表接口时,分页几乎是最常见的需求之一。

刚开始数据只有几百条时,下面这种写法通常没有明显问题:

复制代码
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

第二页:

复制代码
LIMIT 20 OFFSET 20;

第三页:

复制代码
LIMIT 20 OFFSET 40;

但当订单表增长到几十万甚至几百万条以后,问题会逐渐出现:

  • 第一页很快,翻到第1000页明显变慢;

  • 用户连续翻页时出现重复数据;

  • 刚插入一条新记录,下一页突然少了一条;

  • 同一条记录在两个页面重复出现;

  • ORDER BY created_at 后分页结果偶尔变化;

  • Codex为了修复重复数据,开始增加复杂的前端去重逻辑。

这类问题很多时候不是前端分页组件出了问题,而是底层分页方式本身不适合持续变化的大数据表。


一、OFFSET分页为什么越往后越慢?

假设要读取第10000页,每页20条:

复制代码
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 20 OFFSET 199980;

数据库并不是直接"跳到第199980条"。

很多情况下,它仍然需要找到前面的数据,然后将大量记录跳过,最终只返回20条。

可以简单理解成:

复制代码
找到前200000条
↓
丢掉前199980条
↓
返回最后20条

所以随着 OFFSET 越来越大,需要扫描和丢弃的数据也越来越多。

第一页:

复制代码
OFFSET 0

通常很快。

第10000页:

复制代码
OFFSET 199980

成本就完全不同了。


二、OFFSET为什么会导致重复数据?

假设当前数据按创建时间倒序:

复制代码
A
B
C
D
E
F

每页3条。

第一页:

复制代码
A
B
C

用户查看第一页期间,又产生了一条新记录:

复制代码
X
A
B
C
D
E
F

然后用户请求第二页:

复制代码
LIMIT 3 OFFSET 3

结果可能变成:

复制代码
C
D
E

C 已经在第一页出现过一次。

于是用户看到:

复制代码
第一页:A B C
第二页:C D E

问题不是数据库返回错了,而是两次分页请求之间,数据集合发生了变化。


三、数据删除也可能导致漏数据

还是原来的列表:

复制代码
A
B
C
D
E
F

第一页:

复制代码
A
B
C

此时A被删除:

复制代码
B
C
D
E
F

用户请求:

复制代码
OFFSET 3

数据库会从新的第4条开始:

复制代码
E
F

于是:

复制代码
D

直接被跳过去了。

这就是典型的 OFFSET 分页数据漂移。


四、Cursor Pagination是什么?

Cursor Pagination,也叫游标分页。

它不再告诉数据库:

跳过前面多少条。

而是告诉数据库:

从上一页最后一条数据之后继续。

例如第一页:

复制代码
SELECT *
FROM orders
ORDER BY id DESC
LIMIT 20;

假设最后一条:

复制代码
id = 9850

下一页直接查询:

复制代码
SELECT *
FROM orders
WHERE id < 9850
ORDER BY id DESC
LIMIT 20;

再假设第二页最后一条:

复制代码
id = 9830

下一页:

复制代码
WHERE id < 9830

这样不需要扫描并跳过大量历史数据。


五、Cursor为什么更适合不断增长的数据?

假设第一页读取:

复制代码
100
99
98

游标:

复制代码
98

此时又插入:

复制代码
101

列表变成:

复制代码
101
100
99
98
97
96
95

用户下一页仍然执行:

复制代码
WHERE id < 98
ORDER BY id DESC
LIMIT 3;

得到:

复制代码
97
96
95

新插入的101不会影响已经确定的分页边界。

这就是 Cursor Pagination 比 OFFSET 更稳定的原因之一。


六、不要只用created_at作为游标

很多Codex生成的代码会这样写:

复制代码
WHERE created_at < ?
ORDER BY created_at DESC
LIMIT 20;

这看起来合理,但存在一个问题:

多条数据可能拥有相同的 created_at。

例如:

复制代码
id=100  created_at=10:00:00
id=99   created_at=10:00:00
id=98   created_at=10:00:00

如果上一页最后一条是:

复制代码
id=99
created_at=10:00:00

下一页只使用:

复制代码
created_at < 10:00:00

那么:

复制代码
id=98

可能直接被漏掉。


七、使用"时间 + ID"作为复合游标

更稳定的方式是:

复制代码
created_at
+
id

排序:

复制代码
ORDER BY created_at DESC, id DESC

下一页查询:

复制代码
WHERE
  created_at < :createdAt
  OR (
    created_at = :createdAt
    AND id < :id
  )
ORDER BY created_at DESC, id DESC
LIMIT 20;

这样即使多条数据创建时间完全相同,也可以通过 id 保证稳定顺序。

分页系统中有一个非常重要的原则:

排序必须稳定。

如果两次查询对相同数据的顺序可能变化,分页结果就很容易出现重复或遗漏。


八、Cursor不要直接暴露复杂数据库字段

接口可以返回:

复制代码
{
  "items": [],
  "nextCursor": "..."
}

而不是让前端自己组合:

复制代码
createdAt + id

服务端可以将:

复制代码
{
  "createdAt": "2026-08-11T08:00:00Z",
  "id": 9850
}

编码成 Cursor。

例如经过 Base64 或其他可逆编码后:

复制代码
eyJjcmVhdGVkQXQiOi...

下一次请求:

复制代码
GET /api/orders?cursor=eyJjcmV...

服务端负责解析。

这样未来即使分页字段发生变化,前端也不需要知道数据库具体结构。


九、Cursor必须进行校验

不能直接信任客户端传来的 Cursor。

需要检查:

  • 是否可以正确解析;

  • 是否包含必要字段;

  • 时间格式是否合法;

  • ID是否为正确类型;

  • Cursor是否属于当前查询条件;

  • 是否已经过期。

错误 Cursor 应返回明确的参数错误,而不是直接拼进 SQL。

如果使用字符串拼接:

复制代码
const sql = `
  WHERE id < ${cursor}
`;

还可能引入新的安全风险。

应该继续使用数据库参数绑定。


十、筛选条件改变后必须重置Cursor

例如用户当前查看:

复制代码
status=paid

第一页返回:

复制代码
cursor=A

随后用户把筛选条件改为:

复制代码
status=cancelled

此时不能继续使用原来的:

复制代码
cursor=A

因为这个 Cursor 是基于旧查询结果生成的。

更合理的规则是:

复制代码
筛选条件变化
↓
排序变化
↓
搜索关键词变化
↓
Cursor全部重置

否则很容易出现:

  • 空页;

  • 数据跳跃;

  • 部分结果缺失。


十一、Cursor分页不适合所有场景

Cursor 并不是 OFFSET 的完全替代品。

如果产品明确需要:

复制代码
直接跳到第68页

Cursor Pagination 就不太方便。

因为游标天然擅长:

复制代码
上一页
下一页
继续加载
无限滚动

而不是随机定位某个页码。

因此可以根据业务选择。

OFFSET适合

复制代码
后台管理
数据量不大
必须跳页
数据变化不频繁

Cursor适合

复制代码
信息流
订单列表
动态内容
聊天记录
大数据表
无限滚动
实时变化列表

分页方式应该根据实际使用场景决定,而不是看到 Cursor 更先进就全部替换。


十二、分页性能仍然需要索引

把 OFFSET 改成 Cursor 后,如果查询字段没有索引,性能仍然可能很差。

例如:

复制代码
WHERE
  created_at < ?
ORDER BY created_at DESC, id DESC

可以评估对应的复合索引:

复制代码
CREATE INDEX idx_orders_created_id
ON orders(created_at DESC, id DESC);

如果还带有:

复制代码
WHERE status = 'paid'

可能需要结合真实查询模式重新设计索引。

不要直接让 Codex:

给所有字段都加索引。

索引越多并不一定越好,因为它还会增加:

  • INSERT成本;

  • UPDATE成本;

  • 磁盘占用;

  • 维护成本。

应该通过执行计划确认瓶颈。


十三、不要用前端Set去掩盖重复数据

遇到重复分页时,一个很常见的"修复"是:

复制代码
const uniqueItems = Array.from(
  new Map(
    items.map(item => [item.id, item])
  ).values()
);

这确实可以让页面不显示重复项。

但如果根本原因是服务端分页边界错误,那么问题仍然存在。

更严重的是:

前端去重以后,原本应该显示20条:

复制代码
收到20条
↓
删除3条重复
↓
页面只有17条

开发者还可能误以为是数据库少返回了数据。

因此:

前端去重可以作为保护措施,但不能替代正确的分页设计。


十四、测试分页必须模拟数据变化

普通测试可能只是:

复制代码
插入100条
→ 请求第一页
→ 请求第二页

这种测试无法发现真实问题。

应该增加:

场景1:翻页过程中插入数据

复制代码
请求第一页
→ 插入新记录
→ 请求第二页
→ 不允许重复

场景2:翻页过程中删除数据

复制代码
请求第一页
→ 删除前面记录
→ 请求第二页
→ 不允许漏掉原有后续记录

场景3:相同created_at

插入多条相同时间的数据,确认:

复制代码
无重复
无遗漏

场景4:筛选条件变化

复制代码
paid第一页
→ 切换cancelled
→ Cursor必须重置

这些测试比单纯验证 LIMIT 20 更有价值。


十五、让Codex先分析当前分页方式

遇到分页问题时,可以先这样要求:

复制代码
请先不要修改代码。

检查当前分页实现并输出:

1. 使用OFFSET还是Cursor;
2. 排序字段是什么;
3. 排序是否稳定;
4. 是否存在相同排序值;
5. 数据变化后是否会重复或遗漏;
6. 当前查询是否命中索引;
7. 是否适合切换Cursor Pagination。

先确认问题,再决定是否重构分页接口。

不要看到"分页慢"就直接大范围修改数据库和前端。


十六、把分页规则写进AGENTS.md

长期项目可以加入:

复制代码
# 分页规则

- 大数据动态列表优先评估Cursor Pagination
- Cursor排序必须稳定
- 时间排序建议增加唯一ID作为第二排序字段
- 筛选或排序变化必须重置Cursor
- Cursor必须由服务端解析和校验
- 禁止直接拼接Cursor到SQL
- 分页重复问题不能只通过前端去重解决
- 索引必须根据真实查询和执行计划设计
- 修改分页后必须测试插入、删除和相同时间数据

这样 Codex 在新增列表接口时,就会更主动考虑真实数据变化。


十七、Plus还是Pro?

如果主要使用 Codex 处理:

  • 单个分页接口;

  • 普通 SQL;

  • 中小型数据表;

  • 简单索引分析;

  • 单模块测试;

Plus 通常已经能够覆盖多数需求。

如果长期维护:

  • 大型数据库;

  • 多个列表接口;

  • 高访问量系统;

  • 大量 SQL 与执行计划分析;

  • 前后端分页联动;

  • 多轮性能测试;

则可以根据实际使用强度评估 Pro。

对于这种工程任务,Pro 的价值更多是让数据库分析、代码修改和多轮验证保持连续。

不过无论使用哪个方案,分页性能最终仍然取决于正确的数据模型、排序规则和索引设计。

总结

Codex 写出的分页接口越翻越慢、出现重复或漏数据,很多时候并不是分页组件本身的问题,而是 OFFSET 在动态大数据集上的天然限制。

通过 Cursor Pagination、稳定排序、复合游标和合理索引,可以让分页从:

复制代码
第几页?

转变为:

复制代码
从上一条数据之后继续读取。

对于不断增长和变化的列表,这是更加稳定的思路。

真正可靠的分页设计,不只是第一页加载得快,而是在用户不断翻页、数据持续新增和删除的情况下,仍然能够做到:

不重复、不遗漏,而且性能不会随着页码增长快速下降。

CSDN文章描述

本文介绍 Codex 编写分页接口时常见的 OFFSET 性能和数据漂移问题,并通过 Cursor Pagination、稳定排序、复合游标和数据库索引解决分页重复、漏数据与深分页性能下降。

相关推荐
老纪的技术唠嗑局1 小时前
超级干货分享:中小企业使用 OceanBase 实践经验汇总
数据库
CLOUD ACE2 小时前
谷歌云代理|零售商Target 如何利用 Spanner Graph 提升零售发现体验并将数据库维护成本降低 50%
数据库·零售
MuMuMu12232 小时前
工业园区 VOCs 绿岛集中治理:越华环保集团项目技术方案与工程实践
大数据·运维
品牌测评2 小时前
大模型推理算力平台推荐分享|六家平台计费与架构拆解
大数据·人工智能·架构
LedgerNinja2 小时前
WEEX 真实情况如何?交易平台如何判断是否可靠
大数据·人工智能·区块链
微三云 - 廖会灵 (私域系统开发)2 小时前
智慧社区运营破局:“消费返物业费” 模式的数字化架构与落地实践
大数据·架构
少晓年2 小时前
从 MySQL 迁移到人大金仓 KingbaseES:完整指南与实践
数据库·mysql
全栈弄潮儿3 小时前
第一次让 AI 帮你写代码:用 JavaScript 完成一个待办事项清单
chatgpt·openai·ai编程
顿哥GPT3 小时前
2026年8月11日深度剖析:ChatGPT Plus/Pro 与 Codex 技术实战——构建智能代码生成助手
人工智能·chatgpt