ページング処理は、大量の一覧データを一度に返さず、一定件数ずつ取得・表示するための仕組みです。画面表示を軽くし、通信量を抑え、APIやDBの負荷を下げるために使われます。代表的な方法にはlimit/offset方式とcursor方式があり、前者は分かりやすく実装しやすい一方で、件数が増えると遅くなったり、途中でデータが増減すると内容がずれたりします。cursor方式はその弱点を減らしやすい方法ですが、設計の考え方が少し変わります。

1. ページング処理とは
1-1. 大量データを分割して取得する仕組み
ページング処理とは、大量のデータを一度に全部返さず、一定件数ずつ区切って取得する仕組みです。一覧データを扱うAPIや画面では、ほぼ必須になる基本設計のひとつです。
たとえば商品一覧や投稿一覧が1万件あるとして、それを最初の画面表示で全部返すと、通信量も増え、画面描画も重くなります。そこで「最初は20件だけ返す」「次の20件は次の操作で取る」という形に分けます。これがページングです。
初心者のうちは「一覧が長いから見た目のために分ける」と考えがちですが、実際にはAPI、DB、フロントの全部に関係します。ページングは表示の工夫というより、データを無理なく扱うための基本ルールだと捉えると理解しやすいです。
1-2. 一覧画面やAPIで必要になる理由
ページングが必要になる理由は、画面の使いやすさと、システムの負荷を両方守るためです。どちらか一方だけの話ではありません。
一覧画面では、最初から大量のデータを並べると読みづらくなりますし、スマートフォンでは特に重くなりやすいです。API側でも、一度に大量件数を返すとレスポンスが大きくなり、DBの取得コストも増えます。つまり、ページングは「ユーザーが見やすいようにする」と同時に、「システムが無駄に頑張りすぎないようにする」ための仕組みです。
たとえばECサイトの商品一覧、SNSの投稿一覧、管理画面のユーザー一覧などは、どれもページングが前提です。件数がまだ少ないうちは困らなくても、データが増えたときに急に重くなるので、早めに考えておく価値があります。
1-3. ページングしない場合に起きる問題
ページングしないと、表示が重くなり、通信量が増え、サーバーやDBにも余計な負荷がかかります。件数が少ないうちは見えにくいですが、成長すると問題が表に出ます。
たとえば、全件取得するAPIをそのまま一覧画面で使っていると、データが100件から1万件になったときに、一気に表示が遅くなります。さらに、フロント側で不要なデータまで持つことになり、メモリ使用量も増えます。DB側でも毎回大量データを読むことになるため、他の処理へ影響が出やすくなります。
よくあるのは、「最初はテストデータが10件しかないから全件で十分だった」というケースです。本番で件数が増えてから直すと、API、SQL、画面表示の全部を見直す必要が出ます。だからこそ、一覧APIを作る段階でページングを意識するのが大切です。
2. limit/offset方式の基本
2-1. limitとoffsetの意味
limit/offset方式は、何件取るかと、何件目から取るかを指定するページング方法です。最初に覚える方式として分かりやすく、多くの開発現場で使われています。
limit は取得する件数です。たとえば20なら20件取ります。offset は先頭から何件分を飛ばすかです。たとえば offset が40なら、最初の40件を飛ばして、その次から limit 分だけ取ります。つまり、「20件ずつ区切って、何ページ目かを数字で指定する」イメージです。
たとえば1ページ20件表示で、3ページ目を取りたいなら offset は40、limitは20になります。考え方がシンプルなので、管理画面や小規模一覧では今でもよく使われます。まずページングの基本を理解するにはよい方式です。
2-2. SQLとAPIレスポンスの例
limit/offset方式は、SQLでもAPIでも形が分かりやすいのが利点です。初心者が最初に実装しやすいのはこの分かりやすさが大きいです。
SQLでは LIMIT と OFFSET を使って件数と開始位置を指定します。APIでは、クエリパラメータで ?limit=20&offset=40 のように渡すことが多いです。フロント側から見ても「何ページ目か」を計算しやすく、画面のページ番号表示とも相性がよいです。
SELECT id, name, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;
{
"items": [
{ "id": 101, "title": "投稿タイトル1" },
{ "id": 100, "title": "投稿タイトル2" }
],
"limit": 20,
"offset": 40,
"total": 128
}
この例では、41件目から20件を取得しています。注意点は、必ず ORDER BY のような並び順を決めることです。並び順が曖昧だと、同じoffsetでも返る順番が安定せず、ページングとして意味を持ちにくくなります。
2-3. 実装しやすいが件数増加に弱い理由
limit/offset方式は実装しやすい反面、件数が増えるほど遅くなりやすく、データずれも起きやすいという弱点があります。ここがcursor方式と比べるときの重要なポイントです。
offsetが大きいと、DBは先頭から多くの行を数えたり飛ばしたりしてから、目的の件数を返す必要があります。そのため、10件目より10000件目のほうが重くなりやすいです。また、一覧の途中で新しいデータが追加されたり削除されたりすると、同じoffsetでも前回と違うデータが返ることがあります。
たとえば新着投稿一覧で、1ページ目を見たあとに新しい投稿が先頭へ追加されると、2ページ目で前のデータと重複したり、一部を見落としたりすることがあります。つまり、offset方式は「今の順番をそのまま数で区切る」ため、データが動く一覧には弱いのです。
3. cursor方式の基本
3-1. cursorは「次の取得位置」を表す
cursor方式は、何件目かを数で指定するのではなく、次にどこから取るかを示す値で続きを取得する方法です。offset方式の「ページ番号」とは考え方が少し違います。
たとえば、作成日時の新しい順で一覧を返しているなら、「前回最後に返したデータの createdAt と id より後ろを取る」といった考え方になります。この“次の取得位置”を示す値がcursorです。つまり、ページ番号ではなく、一覧の流れの続きを指し示す方法だと言えます。
初心者のうちは少し分かりにくく感じますが、「本の何ページ目」ではなく、「しおりの位置から続きを読む」に近いイメージを持つと理解しやすいです。ページ番号より柔軟ですが、そのぶん返す値や並び順の設計が重要になります。
3-2. nextCursorを返すAPIの形
cursor方式のAPIでは、取得したデータに加えて、次に使うcursorを返す形が基本になります。フロント側はその値を次回リクエストへ渡して続きを取ります。
たとえば、最初のリクエストでは cursor なしで最初の20件を返し、レスポンスに nextCursor を含めます。次の取得では、その nextCursor をクエリパラメータとして送り、続きの20件をもらいます。こうすると、途中でデータが増減しても、offsetよりずれにくくなります。
{
"items": [
{ "id": 201, "title": "投稿A", "createdAt": "2025-06-01T10:00:00Z" },
{ "id": 200, "title": "投稿B", "createdAt": "2025-06-01T09:58:00Z" }
],
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI1LTA2LTAxVDA5OjU4OjAwWiIsImlkIjoyMDB9"
}
この例では、次ページ取得用の値として nextCursor を返しています。注意点は、cursorの中身に意味のある値をそのまま出しすぎないことです。単純なIDや日時を生で返す設計もありますが、後で扱いを変えたいときに困るため、利用側には“続き取得用の値”として扱わせる設計のほうが安全です。
3-3. 無限スクロールとの相性
cursor方式は、無限スクロールのように「次を続けて読み込むUI」と相性がよいです。ページ番号よりも「続きを読む」操作に向いています。
無限スクロールでは、ユーザーは「3ページ目へ移動する」より、「今の続きが欲しい」という使い方をします。このときcursor方式なら、前回の最後を基準に続きを取れるため、UIの考え方と合っています。新着順や時系列順の一覧とも組み合わせやすいです。
たとえばSNSのタイムラインやチャット履歴の読み込みでは、cursor方式がよく使われます。ただし、ページ番号を表示したい管理画面や、「10ページ目へ直接飛びたい」ようなUIには向かないこともあります。フロントの使いやすさまで含めて選ぶことが大切です。
4. limit/offsetとcursorの使い分け
4-1. 管理画面や小規模一覧ならoffsetでもよい
limit/offset方式は、管理画面や小規模データの一覧なら十分実用的です。cursor方式を知らないと不安になるかもしれませんが、offsetがすぐ悪いわけではありません。
たとえば件数がそこまで多くなく、並び順も安定していて、ページ番号で移動したいUIならoffset方式のほうが分かりやすいです。管理画面では「1ページ目、2ページ目、3ページ目」と移動したい場面が多いため、フロントとの相性もよいです。つまり、offsetは簡単なだけでなく、用途によっては自然な選択です。
初心者の段階では、まず小規模一覧でoffset方式を正しく実装できることも十分価値があります。大切なのは「いつ弱点が出るか」を知ったうえで使うことです。最初から何でもcursorにする必要はありません。
4-2. 大量データや更新が多い一覧はcursorが向く
一方で、大量データや更新頻度の高い一覧では、cursor方式のほうが向いていることが多いです。特に時系列データとの相性がよいです。
理由は、offsetが大きくなると遅くなりやすく、途中で新規データ追加や削除があるとページ内容がずれやすいからです。cursor方式なら、「前回最後に見た位置の続き」を取るので、こうしたずれを減らしやすくなります。つまり、データが動く前提の一覧にはcursorが合いやすいのです。
たとえばSNS投稿、通知一覧、監査ログ、取引履歴のような新着がどんどん増えるデータでは、cursor方式を検討する価値があります。逆に、件数が少なく更新も少ないなら、offsetのほうが素直に作れることもあります。方式の選択は性能だけでなく、データの性質を見ることが重要です。
4-3. ソート条件とcursor設計の注意点
cursor方式では、並び順のルールをはっきり決めることがとても重要です。cursorは「次の位置」を示すので、並び順が曖昧だと成立しません。
たとえば createdAt だけで並べると、同じ時刻のデータが複数ある場合に順序が不安定になることがあります。そのため、createdAt に加えて id を組み合わせるなど、一意に順番が決まる条件を持たせることがよくあります。つまり、cursor設計は単に値を返す話ではなく、「順番をどう固定するか」の設計でもあります。
よくある落とし穴は、ソート条件を後から気軽に変えてしまうことです。たとえば昨日までは createdAt 順、今日は人気順、となると同じcursorが意味を持たなくなります。cursor方式では「何の順番で続きなのか」を明確に保つことが大切です。
5. よくある落とし穴
5-1. offsetが大きくなると遅くなる
offset方式でまず知っておきたい落とし穴は、offsetが大きくなるほど遅くなりやすいことです。最初の数ページでは問題なくても、後ろのページで差が出やすくなります。
これは、DBが目的の位置まで多くの行を読み飛ばしてから、必要な件数を返すためです。たとえば offset 20 より offset 20000 のほうが、途中の行を多く処理する必要があります。結果として、大量データでは後ろのページほどレスポンスが重くなりやすいです。
もし管理画面で深いページまでよく見るなら、offset方式のままでよいかを見直す価値があります。少なくとも、「ページングを入れたから速い」とは限らないことは覚えておくとよいです。件数が増えたときにどこで遅くなるかを意識するのが大切です。
5-2. データ追加/削除でページ内容がずれる
offset方式では、一覧の途中でデータが増減すると、ページ内容がずれることがあります。これは初心者が見落としやすいですが、実運用ではかなり大事です。
たとえば新着順一覧で1ページ目を見たあとに新しいデータが先頭へ入ると、2ページ目で前に見たデータがまた出たり、逆に本来見るべきデータが飛ばされたりします。削除でも同じで、途中の件数が詰まることで位置が変わります。つまり、offsetは「今ある順番を数で切る」ため、順番が変わる一覧に弱いです。
更新の多い一覧で「重複表示された」「一部が抜けた」と感じたら、ページング方式そのものを疑う視点が必要です。見た目のバグに見えても、実はoffset方式の性質が原因ということがあります。
5-3. cursorに意味のある値をそのまま出してしまう
cursor方式の落とし穴として、内部で使っている意味のある値をそのままcursorとして公開してしまうことがあります。動くことはあっても、後から設計変更しづらくなります。
たとえば単純に lastId=200 のような形で返すと、利用側がその意味に依存しやすくなります。あとからソート条件を変えたり、createdAtとidの組み合わせへ変えたりしたくなったときに、互換性の問題が出やすいです。cursorは「続きを取るためのトークン」として扱わせるほうが、内部設計を変えやすくなります。
そのため、cursorはエンコードした値やトークンとして返し、クライアントには中身を意識させない設計がよく使われます。大切なのは、cursorを“意味のある公開API項目”というより、“次ページ取得専用の値”として扱うことです。
6. まとめ
6-1. 最初に選ぶ基準
ページング方式を選ぶときは、データ量、更新頻度、画面の使い方の3つを見ると判断しやすいです。どちらか一方が常に正解、とは考えないほうが実務では自然です。
小規模で安定した一覧や、ページ番号移動が必要な管理画面ならlimit/offset方式が分かりやすいです。大量データや更新の多い一覧、無限スクロール型のUIならcursor方式が向きやすいです。つまり、SQLだけで決めるのではなく、APIとフロントの使い方まで合わせて考えることが大切です。
初心者のうちは「まずoffsetを理解し、どこで困るかを知る」「そのうえでcursorの必要性を判断する」という順番でも十分です。最初から高度な方式を選ぶことより、選んだ理由を説明できることのほうが大事です。
6-2. ページングAPI設計チェックリスト
最後に、ページングAPIを設計するときの基本チェックポイントをまとめます。この観点がそろっていると、後から困りにくいAPIになりやすいです。
- 一覧件数が増えたときの負荷を考えているか
- 画面がページ番号型か、無限スクロール型かを確認したか
- 並び順を明確に決めているか
- offset方式なら深いページでの遅さを意識しているか
- cursor方式なら nextCursor の扱いを固定しているか
- 一覧の途中でデータ追加・削除が起きたときのずれを考えているか
- フロント側が無理なく扱えるレスポンス形式になっているか
ページングは「とりあえず20件ずつ返す」だけの機能ではなく、データ量が増えたときの性能や、一覧の正しさにも関わる設計です。小さなAPIでも、早めに考え方を整理しておくと後で作り直しにくくなります。
7. 参考リンク
- PostgreSQL Documentation: LIMIT and OFFSET
https://www.postgresql.org/docs/current/queries-limit.html - MDN: Intersection Observer API(無限スクロール関連の参考)
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API - GraphQL Cursor Connections Specification(cursor方式の考え方の参考)
https://relay.dev/graphql/connections.htm