【入門編】 APIのページネーション設計(Offset-based vs Cursor-based) – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!ネットワークの裏側やAPIのデータが行き交う仕組みを愛してやまないインフラアーキテクトです。

Webアプリケーションを作ったり、フロントエンドとバックエンドをAPIで繋いだりする時、避けて通れないのが「データのページネーション(分割表示)」ですよね。「データベースに100万件のユーザーデータがあるけれど、一度の通信で全部取ってきたら、ブラウザもサーバーもメモリがパンクして息絶えてしまう……!」そんなピンチを救うのがページネーションの技術です。

今回は、このページネーションの代表的な2つの方式である「オフセットベース(Offset-based)」と「カーソルベース(Cursor-based)」について、身近な例えを交えながら優しく紐解いていきたいと思います。一歩ずつ、リラックスして理解していきましょう!

—

1. なぜページネーションが必要なの?(現実世界に例えてみよう)

いきなり難しいコードを見る前に、私たちが普段暮らしている現実世界で考えてみましょう。

例えば、あなたが世界中の本を集めた巨大な図書館の司書だとします。ある日、お客さんがやってきて、「これまでの歴史に発行された全書籍のリストを全部ください!」と言いました。
もし、高さが何キロもあるような巨大な紙の束を一度にカウンタードカーンと渡したらどうなるでしょうか? お客さんは押しつぶされてしまいますし、あなたもその束を用意するだけで一日が終わってしまいますよね。

だからこそ、現実世界では「1ページにつき20冊ずつ、まずは第1ページ目をお渡ししますね。続きが読みたければ『次のページ』をめくってください」という仕組みを使います。これが、APIの世界における「ページネーション」の考え方です。

—

2. 伝統的で分かりやすい「オフセットベース(Offset-based)」

まず最初に学ぶのは、多くのWebサービスで長年使われてきた「オフセットベース」という方式です。

オフセットベースの仕組み

オフセットベースは、よく使われるSQLの LIMIT と OFFSET の概念にとても近いです。「先頭から何件スキップして(Offset)、何件取得するか(Limit)」を指定します。

例えば、URLのクエリパラメータで以下のように指定します。

GET /api/v1/users?limit=20&offset=40

このリクエストの意味は、「先頭から40件のデータを飛ばして(つまり3ページ目までをスキップして)、次の41件目から20件分のデータを取ってきて!」という指示になります。人間にとって「何ページ目」という感覚が非常に分かりやすいのが最大のメリットですね。

オフセットベースの隠れた罠(デメリット)

一見すると完璧に見えるこの仕組みですが、データ量が増えてくるとインフラやデータベースの裏側で悲鳴が上がります。

データベースは、offset=40 と言われると、「上から順に40番目までのデータを実際に数え上げて、そこまでを読み飛ばす」という作業を裏で行っています。もしこれが offset=1000000(100万件飛ばす)だったらどうでしょう? データベースは100万件分のデータを上から順に舐めてから捨てるという、非常に重たい処理を強いられます。

さらに、データを見ている最中に、誰かが新しいデータを一番上に追加したり削除したりすると、「さっき見たデータが次のページにズレて重複して表示される」「データがごっそり抜け落ちる」という、幽霊のようなデータズレ現象(Phantom Read)が発生します。

—

3. 大量データに強い現代の主流「カーソルベース(Cursor-based)」

「オフセットのやり方だと、データが増えたときに遅くなるしズレちゃう……。何か別の賢い方法はないの?」
そこで登場するのが、現代の大規模API(TwitterやInstagramなど)で引っ張りだこの「カーソルベース」です。

カーソルベースの仕組み

カーソルベースは、本のページ番号ではなく、「しおり(ブックマーク)」を使う方法です。

「先ほどのデータリストの一番最後に載っていたユーザーID user_999 の、すぐ次のデータから20件ちょうだい!」というリクエストを送ります。

GET /api/v1/users?limit=20&cursor=user_999

データベースの裏側では、id > 'user_999' という条件を使って、インデックスが効いた高速な場所からピンポイントでデータを20件スパッと切り出します。何万件スキップしようが、データベースの処理速度はほとんど落ちません。これがインフラエンジニアが思わずニンマリしてしまうカーソル方式の強みです。

また、途中で新しいデータが追加されても、「しおり」の位置は変わりませんから、データが重複したり消えたりするトラブルも綺麗に防ぐことができます。

—

4. コードで比較してみよう(Python / Flask風の実装イメージ)

それでは、それぞれの仕組みがサーバーサイドのプログラムでどう表現されるのか、簡単なPythonのコード例で見てみましょう。日本語のコメントを添えているので、じっくり読んでみてください。

オフセットベースのAPI実装イメージ

from flask import Flask, jsonify, request

app = Flask(__name__)


@app.route("/api/v1/users/offset", methods=["GET"])
def get_users_offset():
    # クエリパラメータから取得数(limit)とスキップ数(offset)を受け取る(デフォルト値も設定)
    limit = int(request.args.get("limit", 20))
    offset = int(request.args.get("offset", 0))

    # データベースから指定された範囲のデータを取得するイメージ
    # SQL: SELECT * FROM users LIMIT limit OFFSET offset;
    users_data = database.query(
        "SELECT * FROM users ORDER BY id ASC LIMIT %s OFFSET %s", (limit, offset)
    )

    return jsonify(
        {
            "limit": limit,
            "offset": offset,
            "results": users_data,
        }
    )

カーソルベースのAPI実装イメージ

from flask import Flask, jsonify, request

app = Flask(__name__)


@app.route("/api/v1/users/cursor", methods=["GET"])
def get_users_cursor():
    # 取得数(limit)と、前回の最後の識別子(cursor)を受け取る
    limit = int(request.args.get("limit", 20))
    cursor = request.args.get("cursor", None)

    if cursor is None:
        # 初回リクエスト時は、最初から指定数分を取得する
        query = "SELECT * FROM users ORDER BY id ASC LIMIT %s"
        users_data = database.query(query, (limit,))
    else:
        # カーソル(前回の最後のID)が指定されている場合は、それより大きいものを取得する
        # SQL: SELECT * FROM users WHERE id > cursor ORDER BY id ASC LIMIT limit;
        query = "SELECT * FROM users WHERE id > %s ORDER BY id ASC LIMIT %s"
        users_data = database.query(query, (cursor, limit))

    # 次回のリクエストで使うための「新しいカーソル(取得したリストの最後の要素のID)」を生成
    next_cursor = users_data[-1]["id"] if users_data else None

    return jsonify(
        {
            "limit": limit,
            "next_cursor": next_cursor,  # クライアントはこれを次のリクエストの cursor に指定する
            "results": users_data,
        }
    )

—

5. どっちを選ぶべき? 実務での選び方ガイド

ここまで読んで、「じゃあ結局どっちを使えばいいの?」という疑問が湧いてきますよね。現場での判断基準を整理してみましょう。

  • オフセットベースを選ぶべきケース
  • 管理画面などで「全何ページあるか(総ページ数)」を画面に表示したい場合。
  • ユーザーが「いきなり50ページ目にジャンプしたい」というランダムアクセスを求めている場合。
  • データ量が数千件程度と少なく、パフォーマンス上の問題が絶対に起きないと言い切れる場合。
  • カーソルベースを選ぶべきケース
  • タイムライン形式のように、ひたすら下へスクロールしてデータを読み進めるUIの場合(無限スクロール)。
  • データ量が何万件、何百万件と膨大であり、サービスの成長に伴ってスケールさせたい場合。
  • データのリアルタイム性が高く、途中でデータが追加・削除されても表示の整合性を保ちたい場合。

—

まとめ

今回は、APIのページネーション設計における「オフセットベース」と「カーソルベース」の違いを、郵便配達や図書館の例えを交えて解説しました。

  • オフセットベースは、ページ番号で分かりやすい反面、大量データになるとデータベースが重くなりやすい。
  • カーソルベースは、しおり(IDなど)を使って高速かつ安全にデータを繋いでいく、大量データ時代の優等生。

APIを設計する際は、「このエンドポイントは誰が、どんな画面で、どれくらいのデータ量で使うのか?」を想像しながら、最適な方式を選んでみてくださいね。美しいAPI設計は、快適なネットワークの旅の第一歩です!

コメント

タイトルとURLをコピーしました