列表分页为什么会重复:理解游标和稳定排序
舒心读
18
卷中目录
假设列表按创建时间倒序排列,第一页读取前 10 条,第二页跳过前 10 条。如果两次请求之间又插入了几条新记录,第二次“前 10 条”的含义就变了,读者可能看到重复内容或漏掉记录。
本篇目标: 理解稳定排序与游标边界,知道什么时候适合用游标分页。

排序必须能确定唯一顺序
只按时间排序时,同一时间戳的多行之间可能没有稳定顺序。可以再加入唯一标识作为第二排序键,例如 created_at DESC, id DESC。使用 LIMIT 时,明确排序十分重要,否则不同请求取到的子集可能不一致。PostgreSQL:LIMIT 与 OFFSET
用上一页的末尾作为边界
下面以虚构的 notes 表为例,使用 PostgreSQL 的行值比较。第一页没有边界条件,后续页带上上一页最后一行的时间和 ID。
SELECT id, title, created_at
FROM notes
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 11;
如果页面需要 10 条,可以多取一条判断是否还有下一页。返回给用户前移除额外一条,下一次游标仍取实际展示的最后一行,而不是额外读取的那一行。
游标里还要绑定查询条件
排序方式、搜索条件或可见范围发生变化后,旧游标通常不应继续使用。可以在游标中保存版本与查询指纹,由服务器校验。需要防止篡改时可签名,但 Base64 编码本身不提供保密或真实性。
无论游标看起来多复杂,都不能代替对象权限检查。每次查询仍然要应用当前身份允许的范围。
认识它解决不了的问题
游标分页适合持续向后浏览,不擅长直接跳到任意页。如果排序字段在翻页期间被修改,仍可能出现新的边界问题;严格的历史一致性需要额外快照或固定查询范围。
测试可以包含同时间戳、多条并发插入、删除边界行、修改排序字段和筛选条件变化。先决定读者真正需要的是“逐步浏览”还是“稳定快照”,再选择分页方法,能避免把一种工具当成所有列表的答案。
见字如晤