Offset Pagination — 偏移分页 — 通过 HTTP API 按页加载数据的方法。客户端传输 offset(距开头的偏移量)和 limit(页面大小)参数,服务器从 offset 位置返回记录。根据 REST API Tutorial,这种方法因其实现简单而广泛用于 RESTful 服务中。然而,在大数据量下,偏移分页由于需要完全扫描表到所需位置而失去性能。
要点
Offset Pagination — 是一种按页分割数据的方法,其中客户端请求包含两个参数:offset(跳过多少条记录)和 limit(返回多少条记录)。服务器执行带有 OFFSET 和 LIMIT 的 SQL 查询,跳过指定数量的行,并返回固定大小的结果集。
该方法起源于关系数据库,是组织页面间导航的最简单方式,并随着 REST 架构的发展被迁移到 HTTP API。Offset Pagination 不需要在服务器上存储状态 — 每个请求都是独立的,包含获取数据所需的所有信息。
根据 Postman(2025)的 API 设计研究,偏移分页在 72% 的公共 REST API 中使用,尽管在大数据量下存在已知的性能限制,它仍然是主导标准。
一个典型的带有 Offset Pagination 的 REST 请求包含 offset 和 limit 查询参数。响应包含所请求页面的记录列表和用于构建导航界面的元数据。
limit 参数限制返回的记录数量,保护服务器和客户端免受过度负载。典型的 limit 值 — 根据数据复杂度,每页 10 到 50 条记录。
data class PageRequest(
val offset: Int,
val limit: Int
)
data class PageResponse<T>(
val items: List<T>,
val total: Int,
val hasMore: Boolean
)
fun RetrofitApi.fetchPage(request: PageRequest): Call<PageResponse<Item>>
Offset Pagination 转换为使用 OFFSET 和 FETCH NEXT(或在 MySQL/SQLite 中使用 LIMIT)构造的 SQL 查询。数据库服务器扫描表,跳过等于 offset 的行数,并返回接下来的 limit 行。offset 越大,查询耗时越长。
性能问题与数据库无法直接跳转到 offset 位置有关 — 它必须读取并丢弃所有前面的行。在 offset = 100000 和 limit = 20 的情况下,DBMS 将读取 100020 行并只返回 20 行。
SQL — 服务器执行偏移分页所用的语言。在 PostgreSQL 和 MySQL 中使用 LIMIT,在 SQL Server 和 Oracle 中使用 OFFSET...FETCH。不同的 DBMS 以各自的方式优化此查询,但基本的扫描问题仍然相同。
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
数据一致性 — Offset Pagination 在处理动态数据集时的主要缺点。如果在用户两次请求之间向表开头添加了新记录,所有现有记录都会移位。用户会看到重复或跳过。
想象一个有 100 条记录的表,limit = 20。在第 1 页,用户看到记录 1-20。管理员添加了 5 条新记录。在第 2 页,用户看到记录 26-45 而不是期望的 21-40 — 记录 21-25 被 跳过,而前一组中的记录 21-25 在第 1 页上被 重复。
基于游标的分页 — Offset Pagination 的替代方案,使用指向当前页面最后一条记录的指针。客户端传递最后收到的记录的标识符,而不是数字偏移量,服务器返回其后的 N 条记录。
基于游标的方法解决了一致性问题:游标的位置在插入或删除时不会改变,因为游标引用的是特定记录而不是位置。然而,它实现起来更复杂 — 需要一个唯一可排序的字段(通常是 ID 或时间戳)。
| 参数 | Offset Pagination | 基于游标的分页 |
|---|---|---|
| 简单性 | 高 — 两个数字参数 | 中等 — 需要编码游标 |
| 一致性 | 低 — 插入时重复 | 高 — 游标不依赖于更改 |
| 性能 | 随 offset 增大而下降 | 在任何数据量下稳定 |
| 跳转到页面 | 是 — 可以跳转到任意页面 | 否 — 仅顺序导航 |
| 适用于 | <10K 记录的表,带页码的 UI | 信息流、无限滚动、大数据集 |
两种方法之间的选择取决于用户的 界面 需求。如果需要带页码和直接跳转的导航,Offset Pagination 更简单。对于无限滚动或新闻信息流,游标更可取。
键集分页 — 基于游标方法的一种变体,其中使用 WHERE 而不是 OFFSET 通过唯一键进行过滤。SQL 查询使用条件 WHERE id > lastId,允许数据库使用索引而无需扫描已丢弃的行。
根据 PostgreSQL Wiki,键集分页在大偏移量下比偏移查询快 100-1000 倍,因为索引扫描取代了全表遍历。缺点 — 无法在不按顺序遍历的情况下跳转到任意页面。
Offset Pagination 适用于中小型数据集(最多 10000 条记录),用户需要带页码的界面。典型场景 — 管理面板、订单列表、带过滤和分页的目录。
对于移动应用,偏移分页适用于加载历史数据,其中新记录的插入很少或不可能 — 例如,用户的订单历史、已完成任务列表、交易存档。在这些场景中,不会出现一致性问题。
不建议将 Offset Pagination 用于社交媒体信息流、评论列表、聊天和其他频繁插入的动态数据集。在这些情况下,记录的跳过和重复会恶化用户体验,并需要在客户端进行额外的去重逻辑。
混合分页结合了 offset 和游标:第一个请求使用 offset 显示起始页面,后续请求使用游标加载无限滚动。这种方法在 Instagram 和 Twitter 中实现,其中第一页通过游标加载,但 offset 用于在返回先前视图时计算位置。
混合方法的实现需要在客户端存储用户的虚拟位置,并在服务器上协调两种分页机制。根据 Instagram Engineering 博客,他们的团队使用带有额外 startCursor 字段的基于游标的分页,该字段替换了初始加载的 offset。
移动应用将 Offset Pagination 与 Android 上的 Retrofit/OkHttp 和 iOS 上的 URLSession/Combine 结合使用。典型模式 — 通过 RecyclerView.OnScrollListener 或 UICollectionView prefetching 在滚动到列表末尾时加载下一页。
移动客户端上偏移分页的实现包括三个组件:分页管理器(存储当前 offset 和 hasMore)、列表适配器(显示元素和加载指示器)和存储库(执行请求和处理错误)。Android Jetpack 提供 Paging 3 库,它开箱即用地支持 offset 和基于游标的分页。
Paging 3 — 用于按页加载数据的 Android Jetpack 库。它封装了分页逻辑,包括 offset 跟踪、加载状态管理和滚动时自动加载。PagingSource 定义了下一页和上一页的键。
class OffsetPagingSource(
private val api: ApiService,
private val limit: Int = 20
) : PagingSource<Int, Item>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Item> {
val offset = params.key ?: 0
return try {
val response = api.getItems(offset, limit)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = if (response.hasMore) offset + limit else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource 定义了用于页面导航的 prevKey 和 nextKey 键。在偏移分页中,prevKey 始终为 null(无法在不保存历史记录的情况下转到上一页),而 nextKey 在每次加载时增加 limit,直到服务器返回 hasMore = false。这是一个简单且可预测的移动列表模型。
第一个错误 — 在没有排序的情况下依赖记录的顺序。Offset Pagination 需要在唯一字段上进行稳定的 ORDER BY 排序。没有它,DBMS 可能以任意顺序返回记录,导致页面之间出现随机重复和跳过。
第二个错误 — 使用 offset 计算界面中的 页码。公式 page = offset / limit + 1 仅在加载之间没有记录被删除或添加的条件下有效。在动态数据下,页码变得不准确,用户会看到错误的信息。
第三个错误 — 忽略大 offset 查询的 超时。在 offset 超过 100000 时,查询可能耗时数十秒,阻塞界面并消耗服务器资源。建议在 API 级别设置最大 offset 值(例如 10000),并对大数据量使用基于游标的分页。
第四个错误 — 未在响应中添加 total count。没有总记录数,客户端无法显示页数并实现带数字的分页。然而,在大表上计算 COUNT(*) 同样代价高昂 — 对于超过 100000 条记录的数据集,使用近似估计或限制 maximum total 值。
常见问题
Offset 使用数字偏移量(offset)跳过记录,而游标使用指向上页最后一条记录的指针。Offset 实现更简单,但在插入时会出现重复,在大偏移量下会损失性能。游标在任何数据变化下都是稳定的。
偏移分页在 offset 超过 10000 条记录时由于完全表扫描而 效率低下。它也不适用于动态数据集(信息流、聊天),其中新记录在请求之间出现 — 用户在导航时会看到跳过和重复。
最佳 limit 取决于记录大小和网络速度 — 每页 10 到 50 个元素。对于带有大图像的列表,使用 limit = 10-15,对于文本数据 — 20-50。始终允许客户端指定自己的 limit,并在服务器上设置最大限制(通常为 100)。
处理 重复,请根据唯一 ID 在客户端进行去重,在唯一字段上应用稳定排序,或切换到基于游标的分页。Android Paging 3 支持使用 键 自动去重列表元素。
是的,GraphQL 通过查询中的 offset 和 limit 参数支持偏移分页,尽管 Relay 规范推荐基于游标的方法。Apollo GraphQL 和 Relay 库提供对偏移分页的内置支持,并自动管理页面状态。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。