Skip to content

分页、过滤与排序

本章导读:列表接口是 API 流量最大的入口,三件套(分页/过滤/排序)的参数设计直接决定可用性与性能。本章对比偏移分页与游标分页,给出参数命名、运算符设计、Link 头标准化方案与深分页的工程解法。

1. 集合响应的外壳

所有列表端点复用同一个信封(见响应结构与命名):

jsonc
GET /v1/articles?status=published&sort=-createdAt&page=2&pageSize=20

{
  "items": [ /* … */ ],
  "page": 2,
  "pageSize": 20,
  "total": 214,                  // 可选:总数(深分页/大表下可能昂贵,允许为 null)
  "links": {                     // 可选:HATEOAS 风格;或用 RFC 8288 Link 响应头
    "next": "/v1/articles?status=published&sort=-createdAt&page=3&pageSize=20",
    "prev": "/v1/articles?status=published&sort=-createdAt&page=1&pageSize=20"
  }
}
http
// 等价的头部表达(RFC 8288,GitHub/Google JSON:API 均采用)
Link: <...&page=3>; rel="next", <...&page=1>; rel="prev"

2. 分页模型选择

2.1 偏移分页(page/pageSize 或 offset/limit)

GET /articles?page=2&pageSize=20          ← 页码式
GET /articles?offset=20&limit=20          ← 偏移式(JSON:API 风格)
  • 优点:实现直白(SQL LIMIT/OFFSET)、可随机跳页、能显示"共 N 页"。
  • 缺点:
    1. 数据漂移:翻页间隙有新数据插入 → 条目重复或漏显;
    2. 深分页性能悬崖OFFSET 100000 需要扫过并丢弃十万行。

适用:后台管理、中小数据量、需要跳页的 UI。

2.2 游标分页(cursor-based)

GET /articles?sort=-createdAt&limit=20                  ← 第一页
GET /articles?sort=-createdAt&limit=20&cursor=eyJvIjoiMjAyNi0wOS0xOFQwODozMDowMFoifQ

游标是服务端编码的不透明指针(内部含排序键 + 唯一 ID),客户端不必理解。

  • 优点:结果稳定(快照语义)、性能平坦(WHERE (createdAt, id) < (?, ?) ORDER BY … LIMIT 20 走索引)。
  • 缺点:不能随机跳页;要求排序键唯一(不唯一时追加 id 作 tie-breaker)。

适用:信息流(时间线 feed)、无限滚动、高并发大表。社交 App 的"下拉刷新加载更多"全是游标分页。

2.3 决策表

需求选择
页码导航、总页数展示偏移分页
无限滚动、消息流游标分页
数据量 > 百万且写频繁游标分页(或限制最大偏移 maxOffset=10000
导出/遍历全量游标 + 作业资源,禁止超深偏移

3. 过滤参数设计

3.1 一等公民规则

  • 参数名 = 资源字段名(status=published),保持与 JSON 表示一致;
  • 未识别的参数必须拒绝或告警400 + problem details)——静默忽略会导致客户端误以为过滤生效;
  • 可过滤字段要文档化白名单,不默认全字段开放(防慢查询与探测)。

3.2 常用运算符模式

# 等值 / 多值
?status=published&tag=rest,http
# 比较(概念写法;实际需百分号编码 > <,或改用命名式)
?createdAtGte=2026-09-01T00:00:00Z&viewsLte=1000
# 表达式式过滤(Google AIP-160 风格:单参数 + 字符串表达式)
?filter=createdAt >= "2026-09-01T00:00:00Z" AND views > 1000
# 前缀/模糊
?q=REST&searchFields=title
# 空值
?deletedAt=null
# 组合(AND 默认;OR 显式)
?status=published&authorId=7

另一流派是"后缀运算符"(createdAtGt=…,Microsoft 风格)或"中缀"(createdAt[gt]=…,Laravel/JSON:API 常见方括号式)。没有对错,只有统一——选定一种写进 OpenAPI。

3.3 范围与时间的坑

  • 时间区间统一左闭右开?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z(含 9 月整月),文档写明;
  • 日期参数一律 RFC 3339 或 YYYY-MM-DD,拒绝本地格式;
  • 数值区间用 min/maxgt/ge/lt/le,一套到底。

4. 排序参数设计

?sort=createdAt              ← 单字段升序
?sort=-createdAt             ← 减号前缀 = 降序(GitHub/JSON:API 通用惯例)
?sort=-views,createdAt       ← 多字段:按优先级排列
?sortOrder=desc              ← 与 sortField=title 成对出现(另一流派)

规则:

  1. 默认必须有稳定排序(如 -createdAt,id)——否则翻页会重复/漏数据;
  2. 可排序字段白名单化(排序列 = 索引列,否则大表 ORDER BY 会炸库);
  3. 游标分页的 cursor 必须与 sort 绑定:换排序即失效(服务端校验,不匹配返回 400)。

5. 字段选择与关联扩展(缓解 over-fetching)

GET /articles?fields=items(id,title),total     ← JSON:API 风格(带资源前缀)
GET /articles?fields=id,title,author&expand=author   ← 简化式:选字段 + 展关联
  • fields:控制返回的字段列表,默认返回"安全全集";
  • expand:把外键展开为内嵌对象(author={…}),未展开时只给 { "authorId": 7, "links": { "author": "/users/7" } }
  • 限制 expand 深度与数量(如最多 2 个关联),防止被构造出 N+1 放大器。

6. 边界防御(必做清单)

  • [ ] pageSize/limit 有服务端上限(如 100),超出取上限或 400,禁止无限大
  • [ ] 默认值明确:page=1&pageSize=20
  • [ ] 空列表返回 items: [] + 200,不是 404(集合资源存在,只是为空)
  • [ ] 游标必须签名/加密或存 Redis——防篡改防深翻(cursor 是"不透明"的)
  • [ ] 监控慢查询参数(如 offset 特别大的请求)并告警
  • [ ] total 计算昂贵时可省略或返回 "total": null + "totalRelation": "gte"(近似值语义,Elasticsearch track_total_hits 思路)

7. 本章小结

  • 外壳统一:items + 分页字段 + links;分页语义与 Link 头(RFC 8288)是同一件事的两种表达。
  • 偏移分页赢在跳页,游标分页赢在稳定与性能——按场景选,别混用两套参数。
  • 过滤排序参数无官方标准,命名可以自选,纪律必须统一:白名单、拒绝未知参数、稳定默认排序、上限钳制。

8. 下一步

单个资源的响应长什么样、字段怎么命名 → 响应结构与命名