景泰拾伍.
首页 / 后端开发

翻页时重复、漏数据:先给排序补上唯一键

SQL / 分页接口   ·   整理于 2026-10-30

列表按创建时间倒序,看起来已经有了顺序。但多条记录拥有相同的 created_at 时,数据库不一定每次都以相同顺序返回它们。翻页边界因此可能出现重复或遗漏。

排序必须能打破并列

先给时间字段加上唯一键作为第二排序条件。下面假设 id 为可比较的唯一整数,created_at 非空,且两者在记录创建后不修改。

SELECT id, created_at, title
FROM notes
WHERE owner_id = :owner_id
ORDER BY created_at DESC, id DESC
LIMIT :limit OFFSET :offset;

这解决了相同时间的顺序问题,却不解决翻页期间新增或删除带来的位置移动。第 1 页读完后,顶部插入一条新记录,第 2 页的 OFFSET 就可能再次读到上一页末尾。

按“上次读到的位置”继续查

游标分页把上一页最后一条记录的排序值作为边界,用严格小于条件继续向后读取。

SELECT id, created_at, title
FROM notes
WHERE owner_id = :owner_id
  AND (
    created_at < :cursor_time
    OR (created_at = :cursor_time AND id < :cursor_id)
  )
ORDER BY created_at DESC, id DESC
LIMIT :page_size_plus_one;

多取一条用于判断 hasMore,返回给用户的仍然只有 pageSize 条;下一页游标取已返回记录中的最后一条,不要取额外的那一条。第一页没有游标时,省略边界条件。

游标不是权限凭据

可以将时间和 id 编码成不透明字符串。Base64 只是编码,不防篡改;需要防篡改时使用签名,并验证格式与版本。无论是否签名,每次查询仍必须依据当前用户重新执行权限过滤。

筛选条件和排序改变后,应丢弃旧游标。接口可以把筛选条件摘要绑定到游标中,避免用户拿关键词 A 的游标去翻关键词 B 的结果。

按场景选,不必全部改成游标

后台表格需要跳到第 20 页、显示精确总数时,offset 仍然直观,代价是大偏移查询可能较慢。时间线、日志流、聊天历史更适合游标。游标也不等于完整快照:排序字段或过滤字段变化,仍可能影响遍历结果。

针对上述查询,可以从 (owner_id, created_at, id) 这类联合索引开始评估,结合数据库版本和执行计划确认。不要只因为“加了索引”就认为扫描范围一定变小。

← 返回文章列表