服务端与数据 / 进阶

API 分页:数据会变化时,用游标代替页码

offset 分页简单,但在数据持续新增或删除时会重复或漏掉记录。本文实现基于 created_at 与 id 的稳定游标分页。

页码为什么会漂移

客户端读取第一页后,如果列表顶部插入了新记录,第二页的 OFFSET 会整体后移,上一页末尾的记录可能再次出现。删除记录则可能造成遗漏。OFFSET 越大,数据库跳过的行也越多。

后台管理表格需要跳到任意页且数据变化不频繁时,页码仍然合理;时间线、动态流和大数据集更适合游标。

游标必须对应唯一且稳定的排序

created_at 可能相同,因此只把时间放进游标并不安全。用 created_at 与 id 组成复合排序和复合游标,才能明确上一页最后一条记录之后的位置。

EXAMPLE / 02REST API
SELECT id, title, created_at
FROM posts
WHERE ($1::timestamptz IS NULL)
   OR (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT $3;

const encodeCursor = row => Buffer.from(JSON.stringify({
  createdAt: row.created_at,
  id: row.id
})).toString('base64url');

多取一条判断 hasMore

查询 limit + 1 条。如果多出来一条,去掉它并返回 hasMore=true。客户端不需要知道游标内部结构,服务端也可以在未来调整编码方式。

EXAMPLE / 03REST API
{
  "data": [{ "id": "p_102", "title": "..." }],
  "pageInfo": {
    "nextCursor": "eyJjcmVhdGVkQXQiOi...",
    "hasMore": true
  }
}