项目文件夹

文件
2026-07-13 21:36:01 +08:00

495 行
9.5 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 分页模式
## 为什么需要分页?
大型集合不能一次性全部返回,原因在于:
- 性能问题(查询速度慢、数据载荷大)
- 内存限制(服务端和客户端)
- 网络超时
- 用户体验差
集合端点始终要进行分页。
## 分页策略
### 1. 基于偏移量的分页(Offset-Based Pagination
最常见且直观。使用 `offset`(跳过数量)和 `limit`(每页条数)。
**请求:**
```http
GET /users?offset=20&limit=10
```
**响应:**
```json
{
"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
使用页码简化的偏移量分页。
**请求:**
```http
GET /users?page=3&per_page=10
```
**响应:**
```json
{
"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
使用一个不透明的游标(指针)指向下一组结果。
**请求:**
```http
GET /users?limit=10
GET /users?cursor=eyJpZCI6MTIzfQ&limit=10
```
**响应:**
```json
{
"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 编码):**
```json
{"id": 30, "sort": "created_at"}
```
**实现方式:**
```sql
-- 第一页
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
与游标分页类似,但使用实际的字段值而非不透明的游标。
**请求:**
```http
GET /users?after_id=20&limit=10
GET /users?after_created_at=2024-01-15T10:30:00Z&limit=10
```
**响应:**
```json
{
"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"
}
}
```
**实现方式:**
```sql
SELECT * FROM users
WHERE id > 20
ORDER BY id ASC
LIMIT 10;
```
**优点:**
- 非常高效(利用索引)
- 游标透明(人类可读)
- 结果一致
- 实现简单
**缺点:**
- 需要已建立索引的列
- 不支持随机访问
- 排序仅限于游标字段
- 多字段排序时较复杂
**适用场景:**
- 简单排序(按 ID、时间戳)
- 需要高效分页
- 希望游标透明
- 已有合适的索引
### 5. 查找分页/基于时间的分页(Seek Pagination / Time-Based
针对时序数据专用的键集分页。
**请求:**
```http
GET /events?since=2024-01-15T10:00:00Z&until=2024-01-15T11:00:00Z&limit=100
```
**响应:**
```json
{
"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"
}
}
```
**适用场景:**
- 时序数据
- 日志和事件
- 活动流
- 分析数据
## 默认限制
始终设置合理的默认值和最大限制:
```json
{
"default_limit": 20,
"max_limit": 100,
"min_limit": 1
}
```
**校验:**
```http
GET /users?limit=1000
响应:400 Bad Request
{
"error": {
"code": "INVALID_LIMIT",
"message": "Limit must be between 1 and 100. Default is 20."
}
}
```
## 响应格式
### 标准分页对象
```json
{
"data": [...],
"pagination": {
"limit": 10,
"offset": 20,
"total": 150,
"has_more": true,
"has_previous": true
}
}
```
### 链接头部(RFC 5988
```http
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
### 嵌入式链接
```json
{
"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" }
}
}
```
## 分页与排序
分页时始终支持排序:
```http
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 # 多字段
```
**对于游标分页,游标必须包含排序字段:**
```json
{
"cursor": {
"id": 123,
"created_at": "2024-01-15T10:30:00Z",
"sort_fields": ["created_at", "id"]
}
}
```
## 分页与过滤
将过滤与分页结合使用:
```http
GET /users?status=active&role=admin&offset=0&limit=10
```
**重要提示:** 先过滤再分页:
1. 过滤记录
2. 统计过滤后的结果数
3. 应用分页
4. 返回分页后的子集
## 总条数
### 包含总条数
```json
{
"data": [...],
"pagination": {
"total": 1523,
"limit": 10,
"offset": 20
}
}
```
**优点:**
- 客户端知道结果总数
- 可计算总页数
- 更好的用户体验(显示"第 3 页,共 153 页"
**缺点:**
- COUNT 查询开销大
- 拖慢响应速度
- 对大型或频繁变更的数据集不准确
### 省略总条数
```json
{
"data": [...],
"pagination": {
"has_more": true,
"limit": 10
}
}
```
**适用场景:**
- 大型数据集(COUNT 太慢)
- 实时数据(总数不断变化)
- 游标分页
- 无限滚动 UI
### 可选的总条数
让客户端决定是否请求总条数:
```http
GET /users?limit=10&include_total=true
```
## 边界情况
### 空结果
```json
{
"data": [],
"pagination": {
"offset": 0,
"limit": 10,
"total": 0,
"has_more": false
}
}
```
### 最后一页
```json
{
"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
}
}
```
### 超出范围
```http
GET /users?offset=10000&limit=10
响应:200 OK(空结果)
{
"data": [],
"pagination": {
"offset": 10000,
"limit": 10,
"total": 150,
"has_more": false
}
}
```
或者对于不存在的页面返回 404
```http
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. **设置合理的默认值**——默认每页 20–50 条
3. **强制执行最大限制**——防止过载(最大 100–1000 条)
4. **包含 has_more 标志**——告知客户端是否还有更多结果
5. **提供导航链接**——让获取上一页/下一页变得简单
6. **文档化分页**——说明游标格式、限制、默认值
7. **保持一致性**——在所有端点上使用相同的分页模式
8. **考虑性能**——根据数据大小/类型选择策略
9. **支持排序**——让客户端控制结果顺序
10. **处理边界情况**——空结果、最后一页、无效游标
## 对比矩阵
| 特性 | 偏移量 | 页码 | 游标 | 键集 |
|---------|--------|------|--------|--------|
| 性能 | 偏移量大时差 | 差 | 优秀 | 优秀 |
| 随机访问 | 支持 | 支持 | 不支持 | 不支持 |
| 总条数 | 支持 | 支持 | 不支持 | 可选 |
| 一致性 | 差 | 差 | 优秀 | 优秀 |
| 复杂度 | 简单 | 简单 | 中等 | 中等 |
| 实时数据 | 差 | 差 | 优秀 | 优秀 |
| 数据库负载 | 高 | 高 | 低 | 低 |
| 适用场景 | 小数据集 | Web UI | 信息流/流式数据 | 大型数据集 |