一句话定义
游标分页(Cursor Pagination)是一种不靠“跳过前 N 条”、而靠“从某条记录之后接着取”的分页方式:把上一页最后一条记录的排序值当作书签(这个书签就叫游标,cursor),下一页从书签后面继续读。
偏移量分页慢在哪
最常见的分页写法是 LIMIT 10 OFFSET 20000,也就是“跳过前 20000 条,取 10 条”。问题就在“跳过”上:数据库并没有瞬移到第 20001 条的本事,它通常得沿着索引一条条数过去,把前 20000 条读出来再丢掉。每页 20 条的话,翻到第 500 页就要白数一万条(数字仅为示意)。页码越靠后越慢,而且这个慢是线性增长的。这就是很多后台列表翻到几十页之后开始转圈的原因。
打个生活化的比方
偏移量分页像排队时数人头:“从第 101 个人开始,给我 10 个人。”你必须先把前 100 个人点一遍。
游标分页像接力棒:“刚才排到张三了,从他后面那位开始,给我 10 个人。”直接找到张三,往后取十个就行,前面有多少人都不影响。
它还会漏数据
更隐蔽的问题是“位置会漂移”。偏移量描述的是一个位次,而位次会变。你在拉取一段很长的对话记录,翻到第 2 页时,会话里刚好插进来几条新消息,原来第 21 条就被挤到了第 24 条——你会重复看到它;反过来,如果前面有消息被删除或撤回,就会有一条内容从你眼皮底下滑过去,你永远拿不到它。对一次性导出、数据同步、训练语料抽取来说,这种重复和遗漏都是麻烦。
游标分页不看位次,只看锚点:WHERE (created_at, id) < (上一页最后一条的时间, id) ORDER BY created_at DESC, id DESC LIMIT 20。中途插入新数据不会影响你手里这个锚点,遍历过程是稳定的。
为什么排序键常常要写成“时间 + id”两个字段?因为时间戳可能重复,一旦并列,锚点就不唯一,可能原地打转或跳过数据。补一个唯一且递增的 id 做“决胜局”,游标才靠谱。
和相邻概念的区别
游标分页在数据库圈子里常被叫作键集分页(Keyset Pagination),两者基本是一回事,“游标”更强调把位置编码成字符串交给客户端保管。
| 维度 | 偏移量分页 Offset Pagination | 游标分页 Cursor Pagination |
|---|---|---|
| 翻页方式 | 可跳到任意第 N 页 | 只能上一页 / 下一页式推进 |
| 深翻性能 | 越往后越慢 | 每页成本基本恒定 |
| 数据变动时 | 可能重复或漏记录 | 不受前面增删影响 |
| 实现与索引 | 简单 | 需要稳定唯一的排序键和对应索引 |
| 典型场景 | 后台表格、页面总数已知 | 信息流、消息记录、批量导出与同步 |
实际意义
做 AI 应用时,聊天记录、Agent 运行日志、向量化任务的待处理清单,都是“持续追加、体量很大、只需要顺着往前拉”的数据。这类接口用游标分页,既能避开深翻的性能悬崖,也能保证一次全量拉取不重不漏。反过来,如果用户需要“跳到第 37 页”这种能力,游标给不了,那还是得用偏移量或者别的方案。
对普通职场人来说,一个可观察的现象是:越来越多 App 只让你“往下滑”,不给页码。这不是偷懒,而是产品在“翻得动”和“跳得准”之间做了取舍——面对会不断变化的长列表,稳定地一条条看完,往往比精确跳到某页更有用。
