ページネーション
大量のデータを複数のページに分割して返し、API のレスポンスサイズとレイテンシを制御する手法
API設計
ページネーションとは
ページネーション (Pagination) は、大量のデータを一度に返すのではなく、複数のページに分割して返す API 設計手法である。1 万件のデータを 1 回で返すと、レスポンスサイズが巨大になり、レイテンシが増大し、クライアントのメモリを圧迫する。
3 つの方式
| 方式 | 仕組み | メリット | デメリット |
|---|---|---|---|
| オフセット | ?page=3&limit=20 |
シンプル、任意のページにジャンプ | 大きなオフセットで遅い |
| カーソル | ?cursor=abc123&limit=20 |
大量データでも高速 | 任意のページにジャンプ不可 |
| キーセット | ?after_id=123&limit=20 |
DB インデックスを活用、高速 | ソート順が固定 |
オフセットベース
// GET /users?page=3&limit=20
const offset = (page - 1) * limit; // 40
const users = await db.query('SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2', [limit, offset]);
// レスポンス
{
"data": [...],
"pagination": { "page": 3, "limit": 20, "total": 1500, "totalPages": 75 }
}
問題: OFFSET 10000 は DB が 10,000 行をスキップするため遅い。データが追加・削除されるとページがずれる。
カーソルベース (推奨)
// GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
const cursor = decodeCursor(cursorParam); // { id: 123 }
const users = await db.query(
'SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT $2',
[cursor.id, limit]
);
const nextCursor = encodeCursor({ id: users[users.length - 1].id });
// レスポンス
{
"data": [...],
"pagination": { "nextCursor": "eyJpZCI6MTQzfQ", "hasMore": true }
}
WHERE id > 123 はインデックスを使うため、データ量に関わらず高速だ。
DynamoDB のページネーション
DynamoDB は LastEvaluatedKey でカーソルベースのページネーションを提供する。
const result = await ddb.send(new QueryCommand({
TableName: 'Orders',
KeyConditionExpression: 'userId = :uid',
ExpressionAttributeValues: { ':uid': userId },
Limit: 20,
ExclusiveStartKey: lastKey, // 前回の LastEvaluatedKey
}));
return {
items: result.Items,
nextKey: result.LastEvaluatedKey, // 次のページのカーソル
};
GraphQL のページネーション (Relay Cursor Connection)
query {
users(first: 20, after: "cursor123") {
edges {
node { id name email }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
実務での選択基準
| ケース | 推奨方式 |
|---|---|
| 管理画面のテーブル (ページ番号表示) | オフセット |
| 無限スクロール | カーソル |
| API (大量データ) | カーソル |
| DynamoDB | カーソル (LastEvaluatedKey) |
体系的に学ぶなら関連書籍を参照してほしい。