
使用 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 EC已经在第一页出现过一次。于是用户看到第一页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。例如id100 created_at10:00:00 id99 created_at10:00:00 id98 created_at10:00:00如果上一页最后一条是id99 created_at10:00:00下一页只使用created_at 10:00:00那么id98可能直接被漏掉。七、使用“时间 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?cursoreyJjcmV...服务端负责解析。这样未来即使分页字段发生变化前端也不需要知道数据库具体结构。九、Cursor必须进行校验不能直接信任客户端传来的 Cursor。需要检查是否可以正确解析是否包含必要字段时间格式是否合法ID是否为正确类型Cursor是否属于当前查询条件是否已经过期。错误 Cursor 应返回明确的参数错误而不是直接拼进 SQL。如果使用字符串拼接const sql WHERE id ${cursor} ;还可能引入新的安全风险。应该继续使用数据库参数绑定。十、筛选条件改变后必须重置Cursor例如用户当前查看statuspaid第一页返回cursorA随后用户把筛选条件改为statuscancelled此时不能继续使用原来的cursorA因为这个 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、稳定排序、复合游标和数据库索引解决分页重复、漏数据与深分页性能下降。