移动开发中的 Offset Pagination:什么是以及如何实现

作者: IT Sectr 发布日期: 2026-03-11 阅读时间: 10 分钟

Offset Pagination — 偏移分页 — 通过 HTTP API 按页加载数据的方法。客户端传输 offset(距开头的偏移量)和 limit(页面大小)参数,服务器从 offset 位置返回记录。根据 REST API Tutorial,这种方法因其实现简单而广泛用于 RESTful 服务中。然而,在大数据量下,偏移分页由于需要完全扫描表到所需位置而失去性能。

要点

  • Offset Pagination — 服务器跳过 N 条记录并返回接下来的 M 条记录的分页方法。
  • 实现简单使其成为 REST API 和移动客户端的事实标准。
  • 跳过问题 — 在请求之间插入记录时,用户会看到重复数据。
  • 数据跳跃 — 删除记录会导致页面偏移和内容丢失。
  • 基于游标的分页通过指向最后一条记录的指针而不是偏移量来解决这些问题。

什么是 Offset Pagination?

Offset Pagination — 是一种按页分割数据的方法,其中客户端请求包含两个参数:offset(跳过多少条记录)和 limit(返回多少条记录)。服务器执行带有 OFFSETLIMIT 的 SQL 查询,跳过指定数量的行,并返回固定大小的结果集。

该方法起源于关系数据库,是组织页面间导航的最简单方式,并随着 REST 架构的发展被迁移到 HTTP API。Offset Pagination 不需要在服务器上存储状态 — 每个请求都是独立的,包含获取数据所需的所有信息。

根据 Postman(2025)的 API 设计研究,偏移分页在 72% 的公共 REST API 中使用,尽管在大数据量下存在已知的性能限制,它仍然是主导标准。

请求和响应的结构

一个典型的带有 Offset Pagination 的 REST 请求包含 offset 和 limit 查询参数。响应包含所请求页面的记录列表和用于构建导航界面的元数据。

limit 参数限制返回的记录数量,保护服务器和客户端免受过度负载。典型的 limit 值 — 根据数据复杂度,每页 10 到 50 条记录。

kotlin
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 Pagination 转换为使用 OFFSET 和 FETCH NEXT(或在 MySQL/SQLite 中使用 LIMIT)构造的 SQL 查询。数据库服务器扫描表,跳过等于 offset 的行数,并返回接下来的 limit 行。offset 越大,查询耗时越长。

性能问题与数据库无法直接跳转到 offset 位置有关 — 它必须读取并丢弃所有前面的行。在 offset = 100000 和 limit = 20 的情况下,DBMS 将读取 100020 行并只返回 20 行。

SQL 查询底层原理

SQL — 服务器执行偏移分页所用的语言。在 PostgreSQL 和 MySQL 中使用 LIMIT,在 SQL Server 和 Oracle 中使用 OFFSET...FETCH。不同的 DBMS 以各自的方式优化此查询,但基本的扫描问题仍然相同。

sql
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 vs Cursor-based:方法对比

基于游标的分页 — Offset Pagination 的替代方案,使用指向当前页面最后一条记录的指针。客户端传递最后收到的记录的标识符,而不是数字偏移量,服务器返回其后的 N 条记录。

基于游标的方法解决了一致性问题:游标的位置在插入或删除时不会改变,因为游标引用的是特定记录而不是位置。然而,它实现起来更复杂 — 需要一个唯一可排序的字段(通常是 ID 或时间戳)。

参数Offset Pagination基于游标的分页
简单性高 — 两个数字参数中等 — 需要编码游标
一致性低 — 插入时重复高 — 游标不依赖于更改
性能随 offset 增大而下降在任何数据量下稳定
跳转到页面是 — 可以跳转到任意页面否 — 仅顺序导航
适用于<10K 记录的表,带页码的 UI信息流、无限滚动、大数据集

两种方法之间的选择取决于用户的 界面 需求。如果需要带页码和直接跳转的导航,Offset Pagination 更简单。对于无限滚动或新闻信息流,游标更可取。

键集分页

键集分页 — 基于游标方法的一种变体,其中使用 WHERE 而不是 OFFSET 通过唯一键进行过滤。SQL 查询使用条件 WHERE id > lastId,允许数据库使用索引而无需扫描已丢弃的行。

根据 PostgreSQL Wiki,键集分页在大偏移量下比偏移查询快 100-1000 倍,因为索引扫描取代了全表遍历。缺点 — 无法在不按顺序遍历的情况下跳转到任意页面。

何时使用 Offset Pagination

Offset Pagination 适用于中小型数据集(最多 10000 条记录),用户需要带页码的界面。典型场景 — 管理面板、订单列表、带过滤和分页的目录。

对于移动应用,偏移分页适用于加载历史数据,其中新记录的插入很少或不可能 — 例如,用户的订单历史、已完成任务列表、交易存档。在这些场景中,不会出现一致性问题。

不建议将 Offset Pagination 用于社交媒体信息流、评论列表、聊天和其他频繁插入的动态数据集。在这些情况下,记录的跳过和重复会恶化用户体验,并需要在客户端进行额外的去重逻辑。

混合方法

混合分页结合了 offset 和游标:第一个请求使用 offset 显示起始页面,后续请求使用游标加载无限滚动。这种方法在 Instagram 和 Twitter 中实现,其中第一页通过游标加载,但 offset 用于在返回先前视图时计算位置。

混合方法的实现需要在客户端存储用户的虚拟位置,并在服务器上协调两种分页机制。根据 Instagram Engineering 博客,他们的团队使用带有额外 startCursor 字段的基于游标的分页,该字段替换了初始加载的 offset。

移动应用中的 Offset Pagination

移动应用将 Offset Pagination 与 Android 上的 Retrofit/OkHttp 和 iOS 上的 URLSession/Combine 结合使用。典型模式 — 通过 RecyclerView.OnScrollListener 或 UICollectionView prefetching 在滚动到列表末尾时加载下一页。

移动客户端上偏移分页的实现包括三个组件:分页管理器(存储当前 offset 和 hasMore)、列表适配器(显示元素和加载指示器)和存储库(执行请求和处理错误)。Android Jetpack 提供 Paging 3 库,它开箱即用地支持 offset 和基于游标的分页。

使用 Kotlin 和 Paging 3 实现

Paging 3 — 用于按页加载数据的 Android Jetpack 库。它封装了分页逻辑,包括 offset 跟踪、加载状态管理和滚动时自动加载。PagingSource 定义了下一页和上一页的键。

kotlin
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 的常见错误

第一个错误 — 在没有排序的情况下依赖记录的顺序。Offset Pagination 需要在唯一字段上进行稳定的 ORDER BY 排序。没有它,DBMS 可能以任意顺序返回记录,导致页面之间出现随机重复和跳过。

第二个错误 — 使用 offset 计算界面中的 页码。公式 page = offset / limit + 1 仅在加载之间没有记录被删除或添加的条件下有效。在动态数据下,页码变得不准确,用户会看到错误的信息。

第三个错误 — 忽略大 offset 查询的 超时。在 offset 超过 100000 时,查询可能耗时数十秒,阻塞界面并消耗服务器资源。建议在 API 级别设置最大 offset 值(例如 10000),并对大数据量使用基于游标的分页。

第四个错误 — 未在响应中添加 total count。没有总记录数,客户端无法显示页数并实现带数字的分页。然而,在大表上计算 COUNT(*) 同样代价高昂 — 对于超过 100000 条记录的数据集,使用近似估计或限制 maximum total 值。

常见问题

Offset Pagination 与 Cursor-based 有何不同?

Offset 使用数字偏移量(offset)跳过记录,而游标使用指向上页最后一条记录的指针。Offset 实现更简单,但在插入时会出现重复,在大偏移量下会损失性能。游标在任何数据变化下都是稳定的。

何时 Offset Pagination 表现不佳?

偏移分页在 offset 超过 10000 条记录时由于完全表扫描而 效率低下。它也不适用于动态数据集(信息流、聊天),其中新记录在请求之间出现 — 用户在导航时会看到跳过和重复。

Offset Pagination 的最佳 limit 是多少?

最佳 limit 取决于记录大小和网络速度 — 每页 10 到 50 个元素。对于带有大图像的列表,使用 limit = 10-15,对于文本数据 — 20-50。始终允许客户端指定自己的 limit,并在服务器上设置最大限制(通常为 100)。

如何在 Offset Pagination 中处理重复?

处理 重复,请根据唯一 ID 在客户端进行去重,在唯一字段上应用稳定排序,或切换到基于游标的分页。Android Paging 3 支持使用 自动去重列表元素。

Offset Pagination 可以与 GraphQL 一起使用吗?

是的,GraphQL 通过查询中的 offset 和 limit 参数支持偏移分页,尽管 Relay 规范推荐基于游标的方法。Apollo GraphQL 和 Relay 库提供对偏移分页的内置支持,并自动管理页面状态。

总结

  • Offset Pagination — 使用 offset 和 limit 参数在从 API 按页加载数据时跳过和限制记录的分页方法。
  • 实现简单和请求独立性使 Offset Pagination 成为 72% 的 REST API 的标准方法(Postman,2025 数据)。
  • 性能在 offset 超过 10000 时下降,因为需要扫描表到所需位置 — 数据库读取所有已丢弃的行。
  • 一致性问题 — 查询之间插入和删除记录导致页面结果中出现重复和跳过。
  • 基于游标的分页通过指向最后一条记录的指针而不是数字偏移量来解决 Offset Pagination 的问题。
  • 混合方法结合了第一页的 offset 和游标加载,用于移动应用中的无限滚动。
  • 建议 — 对于最多 10000 条记录的静态数据集使用 Offset Pagination,对于大数据量和动态数据切换到游标。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读