仕組み

ページネーションは、大きな一覧を小分けに取得する方法です。

主な方式は、次の2つです。

方式 取得位置の指定 例
オフセット方式 先頭から何件飛ばすか 100件飛ばして20件
キーセット方式 前回最後の並び順の値 IDが100より大きいものを20件

元の「キーでセット」は、通常 キーセット:keyset を指します。

オフセット方式

SELECT id, title
FROM articles
ORDER BY id ASC
LIMIT 20 OFFSET 100000;

このクエリは、先頭の10万件を飛ばして20件を返します。

ただしDB内部では、飛ばす行にも処理が必要です。そのため、深いページほど負荷が増える場合があります。また、順序を一意に決めないと、ページごとの結果が安定しません。PostgreSQL:LIMITとOFFSET

もう一つの問題は、ページの途中でデータが増減すると、位置がずれることです。

最初の並び:A B C D E F
1ページ目 :A B C

先頭にXが追加:
X A B C D E F

OFFSET 3で次を取得:
C D E

前のページに出たCが再び現れます。

キーセット方式

「3件飛ばす」ではなく、「前回の最後だったCより後」を取得します。

ID順なら、次のように書けます。

SELECT id, title
FROM articles
WHERE id > :last_id
ORDER BY id ASC
LIMIT 20;

新着順の場合は、作成日時だけでは同じ時刻の行を区別できません。IDなどを追加して順序を一意にします。

SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;

これはPostgreSQLでの例です。対応する複合インデックスも重要になります。GitLab:キーセットの性能と順序

カーソルとキーセットの違い

カーソルは、クライアントに渡す「次の取得位置を表す値」です。

{
  "items": [
    { "id": 120, "title": "記事A" },
    { "id": 119, "title": "記事B" }
  ],
  "nextCursor": "opaque-token"
}

キーセットは、その値を使ってDBから取得する方式です。

カーソルの中に単にオフセットを入れる実装も可能なので、カーソル形式のAPIだから高速、とは限りません。

使い分けと注意点

要件 選びやすい方式
任意のページ番号へ移動したい オフセット
小規模な管理画面 オフセット
新着一覧を順に読み進めたい キーセット
大量データを順次取得したい キーセット
取得開始時点の一覧を厳密に再現したい 別途スナップショットなどが必要

キーセットでも、途中で並び順の値が変更されれば、重複や取りこぼしは起こり得ます。また、ページ番号への直接ジャンプは得意ではありません。GitLab:ページネーション設計

設計上の要点
データ件数だけでなく、ページ移動の方法と、途中の更新をどう扱うかで選びます。