Files
2026-07-13 21:36:01 +08:00

9.5 KiB
Raw Permalink Blame History

分页模式

为什么需要分页?

大型集合不能一次性全部返回,原因在于:

  • 性能问题(查询速度慢、数据载荷大)
  • 内存限制(服务端和客户端)
  • 网络超时
  • 用户体验差

集合端点始终要进行分页。

分页策略

1. 基于偏移量的分页(Offset-Based Pagination

最常见且直观。使用 offset(跳过数量)和 limit(每页条数)。

请求:

GET /users?offset=20&limit=10

响应:

{
  "data": [
    {"id": 21, "name": "User 21"},
    {"id": 22, "name": "User 22"}
  ],
  "pagination": {
    "offset": 20,
    "limit": 10,
    "total": 150,
    "has_more": true
  },
  "links": {
    "first": "/users?offset=0&limit=10",
    "prev": "/users?offset=10&limit=10",
    "next": "/users?offset=30&limit=10",
    "last": "/users?offset=140&limit=10"
  }
}

优点:

  • 实现简单
  • 易于理解
  • 支持随机访问(可跳转到任意页)
  • 显示总条数

缺点:

  • 偏移量越大性能越差(数据库需要扫描大量行)
  • 分页过程中数据发生变化会导致结果不一致
  • 对实时数据效率低下
  • 数据库必须计算总行数(开销大)

适用场景:

  • 中小型数据集
  • 数据变更不频繁
  • 需要随机访问页面
  • 需要总条数

2. 基于页码的分页(Page-Based Pagination

使用页码简化的偏移量分页。

请求:

GET /users?page=3&per_page=10

响应:

{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 10,
    "total_pages": 15,
    "total_count": 150
  },
  "links": {
    "first": "/users?page=1&per_page=10",
    "prev": "/users?page=2&per_page=10",
    "next": "/users?page=4&per_page=10",
    "last": "/users?page=15&per_page=10"
  }
}

计算公式:

  • offset = (page - 1) * per_page
  • total_pages = ceil(total_count / per_page)

优缺点与基于偏移量的分页相同,但:

  • 对用户更直观(第 1 页、第 2 页)
  • 在 Web 应用中常见

3. 基于游标的分页(Cursor-Based Pagination

使用一个不透明的游标(指针)指向下一组结果。

请求:

GET /users?limit=10
GET /users?cursor=eyJpZCI6MTIzfQ&limit=10

响应:

{
  "data": [
    {"id": 21, "name": "User 21"},
    {"id": 22, "name": "User 22"}
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6MzB9",
    "prev_cursor": "eyJpZCI6MjB9",
    "has_more": true
  },
  "links": {
    "next": "/users?cursor=eyJpZCI6MzB9&limit=10",
    "prev": "/users?cursor=eyJpZCI6MjB9&limit=10"
  }
}

游标结构(base64 编码):

{"id": 30, "sort": "created_at"}

实现方式:

-- 第一页
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;

-- 下一页(游标指向最后一项)
SELECT * FROM users
WHERE created_at < '2024-01-15T10:30:00Z'
ORDER BY created_at DESC
LIMIT 10;

优点:

  • 结果一致(不会出现跳过或重复项)
  • 对大型数据集效率高
  • 适用于实时数据
  • 无需昂贵的 COUNT 查询
  • 数据库性能更优

缺点:

  • 不支持随机访问(无法跳转到第 10 页)
  • 不提供总条数
  • 实现较复杂
  • 游标不透明(用户无法修改)

适用场景:

  • 大型数据集
  • 数据频繁变更
  • 无限滚动 UI
  • 实时信息流
  • 性能至关重要

4. 键集分页(Keyset Pagination

与游标分页类似,但使用实际的字段值而非不透明的游标。

请求:

GET /users?after_id=20&limit=10
GET /users?after_created_at=2024-01-15T10:30:00Z&limit=10

响应:

{
  "data": [
    {"id": 21, "name": "User 21", "created_at": "2024-01-15T11:00:00Z"},
    {"id": 22, "name": "User 22", "created_at": "2024-01-15T11:30:00Z"}
  ],
  "pagination": {
    "after_id": 30,
    "limit": 10,
    "has_more": true
  },
  "links": {
    "next": "/users?after_id=30&limit=10"
  }
}

实现方式:

SELECT * FROM users
WHERE id > 20
ORDER BY id ASC
LIMIT 10;

优点:

  • 非常高效(利用索引)
  • 游标透明(人类可读)
  • 结果一致
  • 实现简单

缺点:

  • 需要已建立索引的列
  • 不支持随机访问
  • 排序仅限于游标字段
  • 多字段排序时较复杂

适用场景:

  • 简单排序(按 ID、时间戳)
  • 需要高效分页
  • 希望游标透明
  • 已有合适的索引

5. 查找分页/基于时间的分页(Seek Pagination / Time-Based

针对时序数据专用的键集分页。

请求:

GET /events?since=2024-01-15T10:00:00Z&until=2024-01-15T11:00:00Z&limit=100

响应:

{
  "data": [...],
  "pagination": {
    "since": "2024-01-15T10:00:00Z",
    "until": "2024-01-15T11:00:00Z",
    "limit": 100,
    "has_more": true
  },
  "links": {
    "next": "/events?since=2024-01-15T11:00:00Z&until=2024-01-15T12:00:00Z&limit=100"
  }
}

适用场景:

  • 时序数据
  • 日志和事件
  • 活动流
  • 分析数据

默认限制

始终设置合理的默认值和最大限制:

{
  "default_limit": 20,
  "max_limit": 100,
  "min_limit": 1
}

校验:

GET /users?limit=1000

响应:400 Bad Request
{
  "error": {
    "code": "INVALID_LIMIT",
    "message": "Limit must be between 1 and 100. Default is 20."
  }
}

响应格式

标准分页对象

{
  "data": [...],
  "pagination": {
    "limit": 10,
    "offset": 20,
    "total": 150,
    "has_more": true,
    "has_previous": true
  }
}

链接头部(RFC 5988

Link: </users?offset=0&limit=10>; rel="first",
      </users?offset=10&limit=10>; rel="prev",
      </users?offset=30&limit=10>; rel="next",
      </users?offset=140&limit=10>; rel="last"

使用方: GitHub API

嵌入式链接

{
  "data": [...],
  "_links": {
    "self": { "href": "/users?offset=20&limit=10" },
    "first": { "href": "/users?offset=0&limit=10" },
    "prev": { "href": "/users?offset=10&limit=10" },
    "next": { "href": "/users?offset=30&limit=10" },
    "last": { "href": "/users?offset=140&limit=10" }
  }
}

分页与排序

分页时始终支持排序:

GET /users?sort=created_at&order=desc&limit=10
GET /users?sort=-created_at&limit=10                    # 降序
GET /users?sort=last_name,first_name&limit=10           # 多字段

对于游标分页,游标必须包含排序字段:

{
  "cursor": {
    "id": 123,
    "created_at": "2024-01-15T10:30:00Z",
    "sort_fields": ["created_at", "id"]
  }
}

分页与过滤

将过滤与分页结合使用:

GET /users?status=active&role=admin&offset=0&limit=10

重要提示: 先过滤再分页:

  1. 过滤记录
  2. 统计过滤后的结果数
  3. 应用分页
  4. 返回分页后的子集

总条数

包含总条数

{
  "data": [...],
  "pagination": {
    "total": 1523,
    "limit": 10,
    "offset": 20
  }
}

优点:

  • 客户端知道结果总数
  • 可计算总页数
  • 更好的用户体验(显示"第 3 页,共 153 页"

缺点:

  • COUNT 查询开销大
  • 拖慢响应速度
  • 对大型或频繁变更的数据集不准确

省略总条数

{
  "data": [...],
  "pagination": {
    "has_more": true,
    "limit": 10
  }
}

适用场景:

  • 大型数据集(COUNT 太慢)
  • 实时数据(总数不断变化)
  • 游标分页
  • 无限滚动 UI

可选的总条数

让客户端决定是否请求总条数:

GET /users?limit=10&include_total=true

边界情况

空结果

{
  "data": [],
  "pagination": {
    "offset": 0,
    "limit": 10,
    "total": 0,
    "has_more": false
  }
}

最后一页

{
  "data": [{"id": 150, "name": "Last User"}],
  "pagination": {
    "offset": 140,
    "limit": 10,
    "total": 150,
    "has_more": false
  },
  "links": {
    "first": "/users?offset=0&limit=10",
    "prev": "/users?offset=130&limit=10",
    "next": null
  }
}

超出范围

GET /users?offset=10000&limit=10

响应:200 OK(空结果)
{
  "data": [],
  "pagination": {
    "offset": 10000,
    "limit": 10,
    "total": 150,
    "has_more": false
  }
}

或者对于不存在的页面返回 404

GET /users?page=1000&per_page=10

响应:404 Not Found
{
  "error": {
    "code": "PAGE_NOT_FOUND",
    "message": "Page 1000 does not exist. Total pages: 15"
  }
}

最佳实践

  1. 始终对集合进行分页——永远不要返回无限制的列表
  2. 设置合理的默认值——默认每页 2050 条
  3. 强制执行最大限制——防止过载(最大 100–1000 条)
  4. 包含 has_more 标志——告知客户端是否还有更多结果
  5. 提供导航链接——让获取上一页/下一页变得简单
  6. 文档化分页——说明游标格式、限制、默认值
  7. 保持一致性——在所有端点上使用相同的分页模式
  8. 考虑性能——根据数据大小/类型选择策略
  9. 支持排序——让客户端控制结果顺序
  10. 处理边界情况——空结果、最后一页、无效游标

对比矩阵

特性 偏移量 页码 游标 键集
性能 偏移量大时差 优秀 优秀
随机访问 支持 支持 不支持 不支持
总条数 支持 支持 不支持 可选
一致性 优秀 优秀
复杂度 简单 简单 中等 中等
实时数据 优秀 优秀
数据库负载
适用场景 小数据集 Web UI 信息流/流式数据 大型数据集