ページネーション

大量のデータを複数のページに分割して返し、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)

体系的に学ぶなら関連書籍を参照してほしい。

関連用語