用途:在分页变慢、偏移量过大、需要无限滚动或重新设计分页接口时,帮助选择并落地合适的分页策略。核心观点:offset、cursor、keyset 三者在性能与 API 契约上差异根本,客户端一旦依赖便难以更改,需先定清楚。
主要能力:
- 选型发现:先确认排序依据(稳定唯一列、用户自定义排序、全文相关性)、访问模式(仅顺序前进、随机跳页、双向导航)、遍历期间数据是否变动、数据量与最大偏移量、消费方是内部还是公开 API。数据 1 万行内且有索引列时 offset 可行;10 万行以上深分页 offset 必然退化;仅追加类数据(事件日志、动态流)禁用 offset。
- offset 问题诊断:SQL 中 OFFSET 需先生成并丢弃 N 行。可用 EXPLAIN ANALYZE 观察实际扫描行数与返回行数之比。
- 策略选择:需随机跳页则用带 late-join 优化的 offset;不需要随机跳页且排序列唯一则用 keyset;排序基于相关性得分或非唯一列则用带复合决胜键的 cursor。
- offset 优化:内层子查询只扫描索引取 id,再 join 回表取完整行;Prisma 无原生 late-join,需用 $queryRaw。计数昂贵时可用 PostgreSQL 的 pg_class.reltuples 近似计数、计数表或短 TTL 缓存,或展示“10000+ 条结果”。
- keyset:以值而非行号编码位置,索引直接定位。复合键必需:排序列加唯一决胜键(通常 id),WHERE 使用元组比较。需匹配排序的复合索引,如 CREATE INDEX idx_posts_keyset ON posts (created_at DESC, id DESC);无索引则与 offset 一样慢。游标应编码为 base64url 的不透明 token,客户端不应自行构造。可给编码/解码函数示例。可空排序列可用 COALESCE 哨兵值或拆两查询合并,默认前者。非唯一排序列用 (price, id) 复合比较。上一页需反转比较符与排序方向,再在应用层反转结果集。
- cursor 分页:Prisma/TypeORM 用单一唯一字段;Prisma 的 cursor 含游标行本身,需 skip: 1 排除;nextCursor 取最后一行 id,仅在满页时给出。MongoDB 用 _id 的 $lt 比较加倒序排序,ObjectId 自带时间序。
- API 契约:建议响应包为 { data, pagination: { nextCursor, hasMore, total?, page?, pageSize? } }。末页返回 nextCursor: null 而非省略字段;hasMore 在 data.length < pageSize 时为 false;省略 total 需说明原因;游标用 base64url 以通过 URL 编码。
- 失败模式:游标行被删除时 keyset 会从删除位置之后继续,属正确行为但需文档说明;排序列可变(updated_at、score)时遍历可能跳过或重复,不可变排序列(created_at、自增 id)才安全;offset 下总计数漂移会导致重复;ORM 游标查询可能产生 N+1,需开启查询日志并用 include 合并。
使用方式:按发现、问题诊断、策略选择、实现、契约、失败模式六阶段推进,先收集访问模式与数据规模再给建议。
限制:资料未提供具体性能基准数字、支持数据库版本范围、配套工具或托管服务信息,需结合自身数据库实测。