Offset Pagination — オフセットページネーション — HTTP APIを介してデータをページ単位で読み込む方法。クライアントはoffset(開始位置からのオフセット)とlimit(ページサイズ)パラメータを送信し、サーバーはoffsetの位置からレコードを返します。REST API Tutorialによると、このアプローチは実装のシンプルさからRESTfulサービスで広く使用されています。ただし、大量のデータでは、目的の位置までのテーブル全体のスキャンにより、オフセットページネーションのパフォーマンスが低下します。
主要ポイント
Offset Paginationは、クライアントリクエストにoffset(スキップするレコード数)とlimit(返すレコード数)の2つのパラメータを含むデータのページネーション方式です。サーバーはOFFSETとLIMITを含むSQLクエリを実行し、指定された行数をスキップして、固定サイズの結果セットを返します。
この方法は、リレーショナルデータベースにおいてページナビゲーションを整理する最も簡単な方法として生まれ、RESTアーキテクチャの発展とともにHTTP APIに移行されました。Offset Paginationはサーバーに状態を保存する必要がありません — 各リクエストは独立しており、クエリに必要なすべての情報を含んでいます。
Postman(2025)のAPIデザインレポートによると、オフセットページネーションは公開REST APIの72%で使用されており、大規模データセットにおける既知のパフォーマンス制限にもかかわらず、支配的な標準となっています。
Offset Paginationを使用した典型的なRESTリクエストには、クエリパラメータoffsetとlimitが含まれます。レスポンスには、要求されたページのレコードリストと、ナビゲーションインターフェースを構築するためのメタデータが含まれます。
limitパラメータは返されるレコード数を制限し、サーバーとクライアントを過剰な負荷から保護します。データの複雑さに応じて、limitの一般的な値は1ページあたり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クエリに変換されます。データベースサーバーはテーブルをスキャンし、オフセットに等しい行数をスキップして、次のlimit行を返します。オフセットが大きいほど、クエリの実行時間が長くなります。
パフォーマンスの問題は、データベースがオフセット位置に直接ジャンプできないことに起因します — 以前のすべての行を読み取って破棄する必要があります。offset = 100000、limit = 20の場合、DBMSは100,020行を読み取り、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の主な欠点です。2つのユーザーリクエストの間にテーブルの先頭に新しいレコードが追加されると、既存のすべてのレコードがずれます。ユーザーは重複や欠落を目にします。
limit = 20の100レコードのテーブルを考えます。ページ1では、ユーザーはレコード1-20を表示します。管理者が5つの新しいレコードを追加します。ページ2では、ユーザーは期待される21-40ではなくレコード26-45を表示します — レコード21-25はスキップされ、前のセットのレコード21-25はページ1で重複します。
Cursor-basedページネーションはOffset Paginationの代替であり、現在のページの最後のレコードへのポインタを使用します。数値オフセットの代わりに、クライアントは最後に受信したレコードのIDを送信し、サーバーはその後の次のNレコードを返します。
Cursor-basedアプローチは一貫性の問題を解決します:カーソルは位置ではなく特定のレコードを参照するため、挿入や削除によってカーソルの位置は変わりません。ただし、実装はより複雑です — 一意でソート可能なフィールド(通常はIDまたはタイムスタンプ)が必要です。
| パラメータ | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| シンプルさ | 高い — 2つの数値パラメータ | 中程度 — カーソルエンコードが必要 |
| 一貫性 | 低い — 挿入時に重複 | 高い — カーソルは変更の影響を受けない |
| パフォーマンス | 大きなオフセットで低下 | どのボリュームでも安定 |
| ページジャンプ | 可能 — 任意のページに移動可 | 不可 — 順次ナビゲーションのみ |
| 適している | <10Kレコードのテーブル、ページ番号UI | フィード、無限スクロール、大規模セット |
アプローチの選択は、ユーザーのインターフェース要件によって異なります。ページ番号によるナビゲーションと直接ジャンプが必要な場合は、Offset Paginationの方が簡単です。無限スクロールやニュースフィードの場合は、カーソルの方が適しています。
Keysetページネーションは、OFTSETの代わりにWHEREを使用して一意のキーでフィルタリングを行う、cursor-basedアプローチの一種です。SQLクエリはWHERE id > lastIdのような条件を使用し、データベースが破棄された行をスキャンせずにインデックスを使用できるようにします。
PostgreSQL Wikiによると、keysetページネーションは大きなオフセットでオフセットクエリよりも100〜1000倍高速です。インデックススキャンがテーブル全体のスキャンを置き換えるためです。欠点は、順次アクセスなしでは任意のページにジャンプできないことです。
Offset Paginationは、ユーザーがページ番号インターフェースを必要とする小〜中規模のデータセット(最大10,000レコード)に最適です。典型的なシナリオには、管理パネル、注文リスト、ページベースのページネーション付きフィルタリングカタログが含まれます。
モバイルアプリケーションでは、新しい挿入が稀または不可能な履歴データを読み込む場合にオフセットページネーションが適しています — たとえば、ユーザーの注文履歴、完了したタスクリスト、トランザクションアーカイブなどです。これらのシナリオでは、一貫性の問題は発生しません。
ソーシャルメディアフィード、コメントリスト、チャット、その他の頻繁な挿入がある動的セットには推奨されません。これらの場合、欠落や重複レコードがユーザーエクスペリエンスを低下させ、クライアント側での重複排除ロジックが必要になります。
ハイブリッドページネーションは、offsetとcursorを組み合わせます:最初のリクエストはoffsetを使用して初期ページを表示し、後続のリクエストはcursorを使用して無限スクロール読み込みを行います。このアプローチはInstagramやTwitterで使用されており、最初のページはcursorで読み込まれますが、以前のビューに戻る際に位置を計算するためにoffsetが使用されます。
ハイブリッドアプローチを実装するには、クライアント側でユーザーの仮想位置を保存し、サーバー側で2つのページネーションメカニズムを調整する必要があります。Instagram Engineeringブログによると、彼らのチームは初期読み込み用のoffsetを置き換える追加のstartCursorフィールドを持つcursor-basedページネーションを使用しています。
モバイルアプリケーションは、AndroidではRetrofit/OkHttp、iOSではURLSession/CombineとともにOffset Paginationを使用します。典型的なパターンは、RecyclerView.OnScrollListenerやUICollectionViewの先読みを介してリストの最後までスクロールしたときに次のページを読み込むことです。
モバイルクライアントでのオフセットページネーションの実装には、3つのコンポーネントが含まれます:ページネーションマネージャー(現在のoffsetとhasMoreを保存)、リストアダプター(アイテムと読み込みインジケーターを表示)、リポジトリ(リクエストを実行しエラーを処理)。Android JetpackはPaging 3ライブラリを提供しており、offsetとcursor-basedの両方のページネーションを標準でサポートしています。
Paging 3は、ページネーションデータ読み込みのためのAndroid Jetpackライブラリです。オフセット追跡、読み込み状態管理、スクロール時の自動プリフェッチを含むページネーションロジックをカプセル化します。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はサーバーがhasMore = falseを返すまで、読み込みごとにlimitずつ増加します。これはモバイルリストのためのシンプルで予測可能なモデルです。
最初の誤り — ソートなしでレコードの順序に依存すること。Offset Paginationは一意のフィールドによる安定したORDER BYソートが必要です。これがないと、DBMSは任意の順序でレコードを返す可能性があり、ページ間でランダムな重複や欠落が発生します。
2番目の誤り — UIでページ番号を計算するためにoffsetを使用すること。page = offset / limit + 1の式は、読み込み間にレコードが削除または追加されなかった場合にのみ機能します。動的データでは、ページ番号が不正確になり、ユーザーは誤った情報を目にします。
3番目の誤り — 大きなoffsetでのクエリのタイムアウトを無視すること。offsetが100,000を超えると、クエリに数十秒かかり、UIをブロックし、サーバーリソースを消費する可能性があります。APIレベルで最大offset値(例:10,000)を設定し、大規模ボリュームにはcursor-basedページネーションを使用することを推奨します。
4番目の誤り — レスポンスにtotal countを含めないこと。レコードの総数がないと、クライアントはページ数を表示できず、番号付きページネーションを実装できません。ただし、大きなテーブルでのCOUNT(*)はコストがかかります — 100,000レコードを超えるセットの場合は、概算推定値を使用するか、最大合計値を制限してください。
よくある質問
Offsetはレコードをスキップするために数値オフセットを使用し、cursorは前のページの最後のレコードへのポインタを使用します。Offsetは実装が簡単ですが、挿入時の重複や大きなオフセットでのパフォーマンス低下に悩まされます。Cursorはデータの変更に対して安定しています。
オフセットページネーションは、テーブル全体のスキャンにより、10,000レコードを超えるオフセットで非効率です。また、リクエスト間に新しいレコードが表示される動的セット(フィード、チャット)にも不適切です — ユーザーはナビゲーション中に欠落や重複レコードを目にします。
最適なlimitはレコードサイズとネットワーク速度によって異なります — 1ページあたり10〜50アイテム。大きな画像を含むリストの場合はlimit = 10-15、テキストデータの場合は20-50。常にクライアントがサーバー側の最大制限(通常100)付きで独自のlimitを指定できるようにしてください。
重複を処理するには、一意のIDによるクライアント側の重複排除を使用するか、一意のフィールドで安定したソートを適用するか、cursor-basedページネーションに切り替えてください。Android Paging 3は、リストアイテムの自動重複排除のためのkeyをサポートしています。
はい、GraphQLはクエリのoffsetおよびlimit引数を通じてオフセットページネーションをサポートしていますが、Relay仕様ではcursor-basedアプローチを推奨しています。Apollo GraphQLおよびRelayライブラリは、自動ページ状態管理付きのオフセットページネーションの組み込みサポートを提供しています。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。