要約
大規模なデータセットでは、オフセットベースのページネーションの代わりにカーソルベースまたはキーセットベースのページネーションを使用してください。オフセットページネーション(?page=1&limit=20)は、数百万のレコードに対してパフォーマンスが低く、データの一貫性を損なう可能性があります。Modern PetstoreAPIは、効率的で一貫した結果を得るために、不透明なトークンとHATEOASリンクを備えたカーソルベースのページネーションを実装しています。
はじめに
あなたのAPIはペットのリストを返します。データベースには1000万匹のペットがいます。クライアントがGET /pets?page=500000&limit=20をリクエストしました。あなたのデータベースはOFFSET 10000000 LIMIT 20を実行します。このクエリには30秒かかり、あなたのAPIはタイムアウトします。
これがオフセットページネーションの問題です。小規模なデータセットでは問題なく機能しますが、大規模になると破綻します。20件の結果しか返さないにもかかわらず、データベースはオフセットに到達するために数百万行をスキャンしなければなりません。
以前のSwagger Petstoreはページネーションに全く対応していません。Modern PetstoreAPIは、数百万のレコードにも一貫したパフォーマンスで対応できるカーソルベースのページネーションを実装しています。
このガイドでは、なぜオフセットページネーションが失敗するのか、カーソルベースのページネーションがどのように機能するのか、そしてModern PetstoreAPIが効率的なページネーションをどのように実装しているのかを学びます。
なぜオフセットページネーションは大規模な環境で失敗するのか
オフセットページネーションは最も一般的なアプローチですが、深刻な問題を抱えています。
オフセットページネーションの仕組み
GET /pets?page=1&limit=20 → OFFSET 0 LIMIT 20
GET /pets?page=2&limit=20 → OFFSET 20 LIMIT 20
GET /pets?page=3&limit=20 → OFFSET 40 LIMIT 20
データベースはoffset行をスキップし、limit行を返します。
問題1:ページ番号が増加するにつれてパフォーマンスが低下する
1ページ目:
SELECT * FROM pets OFFSET 0 LIMIT 20;
-- 高速:20行をスキャン
1000ページ目:
SELECT * FROM pets OFFSET 20000 LIMIT 20;
-- 低速:20,020行をスキャンし、20行を返す
500,000ページ目:
SELECT * FROM pets OFFSET 10000000 LIMIT 20;
-- 非常に低速:10,000,020行をスキャンし、20行を返す
データベースは、破棄する行であっても、オフセットまでのすべての行をスキャンしなければなりません。パフォーマンスはページ番号に比例して低下します。
問題2:結果の一貫性がない
クライアントが結果をページングしている間に、データが変更されると:
リクエスト1:
GET /pets?page=1&limit=2
Returns: [Pet A, Pet B]
誰かがPet Zを追加しました(アルファベット順で最初にソートされる)
リクエスト2:
GET /pets?page=2&limit=2
Returns: [Pet B, Pet C] ← Pet Bが2回出現!
新しいペットが挿入されたため、Pet Bが両方のページに表示されました。逆に、削除が発生した場合は、ペットがスキップされる可能性があります。
問題3:深いページネーションは高価である
ユーザーが10ページ目以降に進むことはほとんどありません。しかし、APIが?page=1000000を許可する場合、それに対応しなければなりません。深いページネーションクエリは高価であり、サービス拒否攻撃に利用される可能性があります。
オフセットページネーションが許容される場合
- 小規模なデータセット(10,000件未満)
- 使用が管理された内部API
- ユーザーが深くページングしない管理インターフェース
- 頻繁に変更されないデータ
公開APIや大規模なデータセットには、カーソルベースのページネーションを使用してください。
カーソルベースのページネーションの解説
カーソルベースのページネーションは、結果セット内の位置を示すために不透明なトークンを使用します。
仕組み
リクエスト1:
GET /pets?limit=20
レスポンス1:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9",
"hasMore": true
}
}
リクエスト2:
GET /pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20
カーソルは、位置をエンコードする不透明なトークン(通常はbase64エンコード)です。クライアントはそれを解析せず、単にAPIに渡すだけです。
利点
1. 一貫したパフォーマンス
データベースはインデックスを使用してカーソル位置を直接見つけます。
SELECT * FROM pets
WHERE id > '019b4132-70aa-764f-b315-e2803d882a24'
ORDER BY id
LIMIT 20;
このクエリは、データセット内の位置に関係なく高速です。これはスキャンではなく、インデックスシークを使用します。
2. 一貫した結果
カーソルは安定しています。リクエスト間でデータが変更されても、一貫した結果が得られます。新しいレコードによって重複やスキップが発生することはありません。
3. 深いページネーション攻撃の防止
クライアントは任意の場所へジャンプできません。シーケンシャルにページングする必要があるため、悪用が制限されます。
カーソル形式
カーソルは通常、base64エンコードされたJSONです。
// デコードされたカーソル
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"createdAt": "2026-03-13T10:30:00Z"
}
カーソルには、ページネーションを再開するために十分な情報が含まれています。Modern PetstoreAPIの場合、これにはリソースIDとソートフィールドが含まれます。
ソートされたデータのためのキーセットページネーション
キーセットページネーションは、ソートされたデータのためのカーソルベースのページネーションのバリエーションです。
仕組み
不透明なカーソルの代わりに、前のページからの最後の値を使用します。
リクエスト1:
GET /pets?limit=20&sortBy=createdAt
レスポンス1:
{
"data": [
{"id": "...", "createdAt": "2026-03-13T10:00:00Z"},
...
{"id": "...", "createdAt": "2026-03-13T10:30:00Z"}
]
}
リクエスト2:
GET /pets?limit=20&sortBy=createdAt&after=2026-03-13T10:30:00Z
afterパラメーターは、前のページの最後のcreatedAt値を使用します。
SQLクエリ
SELECT * FROM pets
WHERE created_at > '2026-03-13T10:30:00Z'
ORDER BY created_at
LIMIT 20;
これは`created_at`にインデックスを使用するため効率的です。
キーセットページネーションを使用する場合
- データが自然にソートされている(タイムスタンプ、IDなど)
- クライアントがページネーションキーを理解する必要がある
- 透明なページネーションを望む(不透明なカーソルではない)
Modern PetstoreAPIはデフォルトでカーソルベースのページネーションを使用しますが、時系列データに対してはキーセットページネーションをサポートしています。
Modern PetstoreAPIはどのようにページネーションを実装しているか
Modern PetstoreAPIは、HATEOASリンクを備えたカーソルベースのページネーションを使用しています。
リクエスト形式
GET /pets?limit=20
GET /pets?cursor={token}&limit=20
パラメーター:
limit- 1ページあたりの結果数(デフォルト:20、最大:100)cursor- 前のレスポンスからの不透明なページネーショントークン
レスポンス形式
{
"data": [
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
],
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9"
},
"links": {
"self": "https://petstoreapi.com/pets?limit=20",
"next": "https://petstoreapi.com/pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20"
}
}
主要な機能
1. 不透明なカーソル
カーソルはbase64エンコードされています。クライアントはそれを解析しません。
2. HATEOASリンク
linksオブジェクトは、すぐに使用できるURLを提供します。クライアントはページネーションURLを構築する必要がありません。
3. hasMoreフラグ
さらに結果があるかどうかを示します。クライアントはいつページングを停止するかを知ることができます。
4. リミットの検証
最大リミットは100です。クライアントが巨大なページをリクエストするのを防ぎます。
詳細については、Modern PetstoreAPIのページネーションドキュメントを参照してください。
ページネーションのレスポンス形式
Modern PetstoreAPIは、ページネーションされたレスポンスを一貫した構造でラップします。
コレクションラッパー
{
"data": [...],
"pagination": {...},
"links": {...}
}
なぜコレクションをラップするのか?
- 拡張性 - クライアントを壊すことなくメタデータを追加できる
- 一貫性 - すべてのページネーションエンドポイントは同じ形式を使用する
- HATEOAS - リンクはクライアントをページネーションに導く
ページネーションメタデータ
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "...",
"totalCount": 1000 // オプション、計算コストが高い
}
totalCountは、大規模なデータセットでは計算コストが高いためオプションです。ほとんどのクライアントはこれを必要としません。
Apidogによるページネーションのテスト
Apidogは、ページネーションの動作を包括的にテストするのに役立ちます。
テストシナリオ
1. 最初のページ
GET /pets?limit=20
Expect: 20 results, hasMore=true, nextCursor present
期待:20件の結果、hasMore=true、nextCursorが存在する
2. 以降のページ
GET /pets?cursor={token}&limit=20
Expect: 20 results, hasMore=true/false, nextCursor present/absent
期待:20件の結果、hasMore=true/false、nextCursorが存在/存在しない
3. 最後のページ
GET /pets?cursor={lastToken}&limit=20
Expect: < 20 results, hasMore=false, no nextCursor
期待:20件未満の結果、hasMore=false、nextCursorがない
4. 空の結果
GET /pets?status=NONEXISTENT&limit=20
Expect: 0 results, hasMore=false, no nextCursor
期待:0件の結果、hasMore=false、nextCursorがない
5. リミットの検証
GET /pets?limit=1000
Expect: 400 Bad Request (exceeds max limit)
期待:400 Bad Request(最大リミットを超過)
Apidogテスト設定
// テスト:ページネーション構造
pm.test("Response has pagination", () => {
pm.expect(pm.response.json()).to.have.property('pagination');
pm.expect(pm.response.json().pagination).to.have.property('hasMore');
});
// テスト:HATEOASリンク
pm.test("Response has links", () => {
const links = pm.response.json().links;
pm.expect(links).to.have.property('self');
if (pm.response.json().pagination.hasMore) {
pm.expect(links).to.have.property('next');
}
});
適切なページネーション戦略の選択
さまざまな戦略が、さまざまなユースケースに適合します。
オフセットページネーション
次の場合に利用:
- データセットが小さい(10,000件未満)
- ユーザーがランダムアクセスを必要とする(50ページにジャンプする)
- データが頻繁に変更されない
- 使用が管理された内部API
次の場合に利用しない:
- データセットが大きい(100,000件以上)
- パフォーマンスが重要である
- データが頻繁に変更される
カーソルベースのページネーション
次の場合に利用:
- データセットが大きい
- パフォーマンスが重要である
- データが頻繁に変更される
- シーケンシャルアクセスで十分である
次の場合に利用しない:
- ユーザーがランダムアクセスを必要とする
- カーソルの複雑さが懸念される
キーセットページネーション
次の場合に利用:
- データが自然にソートされている
- 透明なページネーションが好ましい
- パフォーマンスが重要である
次の場合に利用しない:
- ソート順が複雑である
- 複数のソートフィールドが必要である
Modern PetstoreAPIの推奨事項: 公開APIおよび大規模データセットにはカーソルベースのページネーションを使用してください。
結論
ページネーションは、大規模なデータセットを返すAPIにとって不可欠です。オフセットページネーションはシンプルですが、スケーラビリティがありません。カーソルベースのページネーションは、数百万のレコードに対して一貫したパフォーマンスと信頼性の高い結果を提供します。
Modern PetstoreAPIは、不透明なトークン、HATEOASリンク、適切なメタデータを含むカーソルベースのページネーションを実装しています。この設計は効率的にスケーリングし、優れた開発者エクスペリエンスを提供します。
エッジケースを処理し、制限を検証し、一貫した結果を返すことを確認するために、Apidogを使用してページネーションの実装をテストしてください。
主なポイント:
- 大規模なデータセットではオフセットページネーションを避ける
- スケーラビリティのためにカーソルベースのページネーションを使用する
- コレクションをメタデータとリンクでラップする
- Apidogでページネーションを徹底的にテストする
- Modern PetstoreAPIのページネーションパターンに従う
よくある質問
ページネーションなしで全件を返すのはなぜいけないのですか?
1つのレスポンスで数百万件のレコードを返すと、メモリの問題、ネットワーク転送の遅延、ユーザーエクスペリエンスの低下につながります。ページネーションは大規模なデータセットにとって不可欠です。
カーソルページネーションでクライアントは特定のページにジャンプできますか?
いいえ、カーソルページネーションはシーケンシャルアクセスを必要とします。ランダムアクセスが必要な場合は、小規模なデータセットにはオフセットページネーションを検討するか、代わりに検索/フィルタリングを実装してください。
フィルタリングとページネーションをどのように扱えばよいですか?
ページネーションリクエストにフィルタパラメーターを含めます:GET /pets?status=AVAILABLE&cursor={token}&limit=20。カーソルは位置とフィルタ状態の両方をエンコードします。
ページネーションレスポンスに総件数を含めるべきですか?
クライアントが必要とし、データセットが小さい場合にのみ含めてください。大規模なデータセットでは総件数の計算はコストが高くなります(別途COUNTクエリが必要になります)。
SQLでカーソルページネーションを実装するにはどうすればよいですか?
カーソル値を含むWHERE句を使用します:SELECT * FROM pets WHERE id > ? ORDER BY id LIMIT 20。ソート列にインデックスがあることを確認してください。
カーソルトークンが無効になった場合はどうなりますか?
エラーメッセージとともに400 Bad Requestを返します。データが削除されたり、ページネーションの状態が期限切れになったりすると、カーソルは無効になる可能性があります。
カーソルはどのくらい有効であるべきですか?
Modern PetstoreAPIのカーソルは、参照されるリソースが存在する限り無期限に有効です。一部のAPIでは、カーソルを24時間後に期限切れにします。
複数のソートフィールドでカーソルページネーションを使用できますか?
はい、可能ですが、カーソルはすべてのソートフィールドをエンコードする必要があります。これによりカーソルはより複雑になります。代わりに単一の複合ソートキーを使用することを検討してください。
