Skip to content

ハウツー: オフセット & カーソルページネーション

FT リファレンス: FT325 (NENE2-FT/pagelog) — デュアルページネーション戦略(オフセットベースとカーソルベース)、next_offset/next_cursorhas_more、カテゴリフィルター、15 テスト / 47 アサーション PASS。

このガイドでは同一リソースにオフセットベースとカーソルベースの両方のページネーションエンドポイントを実装する方法を解説します。クライアントはユースケースに合った戦略を選択できます。

スキーマ

sql
CREATE TABLE articles (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    title      TEXT    NOT NULL,
    author     TEXT    NOT NULL,
    category   TEXT    NOT NULL DEFAULT 'general',
    created_at TEXT    NOT NULL
);

エンドポイント

メソッドパス説明
POST/articles記事を作成する
GET/articles/offsetオフセットページネーション
GET/articles/cursorカーソルページネーション
GET/articles/by-categoryカテゴリフィルター

オフセットページネーション

GET /articles/offset?limit=10&offset=0
→ 200
{
  "items": [...],     // 10 件
  "total": 25,
  "limit": 10,
  "offset": 0,
  "has_more": true,
  "next_offset": 10   // 最終ページでは null
}

// ページ 2
GET /articles/offset?limit=10&offset=10
→ {"items": [...], "has_more": true, "next_offset": 20}

// 最終ページ
GET /articles/offset?limit=10&offset=20
→ {"items": [...], "has_more": false, "next_offset": null}

// 範囲外
GET /articles/offset?limit=10&offset=100
→ {"items": [], "has_more": false}

has_more の場合は next_offset = offset + limit、そうでない場合は null

カーソルページネーション

GET /articles/cursor?limit=10
→ 200
{
  "items": [...],        // 新しい順
  "has_more": true,
  "next_cursor": 15      // 最後に返されたアイテムの id
}

// カーソルを使った次のページ
GET /articles/cursor?limit=10&after=15
→ {"items": [...], "has_more": true, "next_cursor": 5}

// 最終ページ
GET /articles/cursor?limit=10&after=5
→ {"items": [...], "has_more": false, "next_cursor": null}

カーソルは最後に返されたアイテムの id です: WHERE id < $after ORDER BY id DESC LIMIT $limit + 1has_more を判定するために 1 件余分に取得します)。

カテゴリフィルター

GET /articles/by-category?category=tech&limit=5
→ {"items": [...], "total": N}

オフセット vs カーソル — 使い分け

基準オフセットカーソル
ランダムなページジャンプ?offset=50❌ 順に辿る必要あり
合計件数が必要✅ 常に含まれる❌ コスト高
挿入時の一貫した結果❌ 新行がページをずらす✅ 安定
大規模データセットのパフォーマンスOFFSET N で N 行スキャンWHERE id < X でインデックス使用
無限スクロール / フィード

してはいけないこと

アンチパターンリスク
最終ページでも next_offset を返すクライアントが余分な空のリクエストをする
数百万行のテーブルで OFFSET N を使うDB が結果を返す前に N 行スキャンする。大規模データにはカーソルを使う
カーソルレスポンスから has_more を省略するクライアントが次のページを取得するかどうか判断できない
タイムスタンプをカーソルとして使う重複したタイムスタンプにより行がスキップまたは重複する。ユニークな整数 id を使う

MIT ライセンスの下で公開されています。