API / Option

cursor

Prisma クエリでページネーションの開始位置を指定するオプション。大量データでも一定のパフォーマンスを保てる cursor ベースページネーションの中核。

prismapaginationcursorkeyset
所属:Prisma Client

代表的な値 / 使い方

  • cursor: { id: lastId }
  • cursor: { id: Number(cursorParam) }
  • cursor: { createdAt: new Date(cursorParam) }

注意点 / Pitfalls

  • ·cursor には一意かつソートキーと一致するフィールドを指定する必要がある(通常は id)
  • ·cursor で指定した行自身を除外するために skip: 1 が必要(省略するとカーソル行が重複して返る)
  • ·offset(skip)方式と異なり totalCount / totalPages の算出が難しく、ページ番号 UI との相性が悪い
  • ·cursor に使うフィールドに DB インデックスがないとパフォーマンス改善が見込めない

一緒に使う項目

補足

cursor ベースは skip の値が増えるほど遅くなる offset 方式の課題を回避できる。無限スクロールや「次へ」だけの UI に向く。ページ番号で任意のページに飛ぶ UI が必要な場合は offset 方式の方がシンプル。

何をするオプションか

cursor は Prisma のカーソルベースページネーションで「どのレコードの次から取得するか」を指定するオプションです。レコードの idcreatedAt などの一意フィールドを位置マーカーとして使います。

prisma.post.findMany({
  cursor: { id: lastId }, // このレコードの次から取得
  take: 10,
  skip: 1,               // cursor 行自身をスキップ
  orderBy: { id: "desc" },
});

skip: 1 が必要な理由

Prisma の cursor は「そのレコード から 始める」という意味です。skip: 1 を省略すると、カーソルで指定したレコード自身も結果に含まれ、前のページの最後の要素が重複して返ります。

// ❌ skip: 1 なし → cursor 行が重複して返る
prisma.post.findMany({ cursor: { id: 42 }, take: 5 });
// 返り値: [id:42, id:41, id:40, id:39, id:38]
//              ↑ 前ページで既に返したレコード

// ✅ skip: 1 あり → cursor 行の次から返る
prisma.post.findMany({ cursor: { id: 42 }, take: 5, skip: 1 });
// 返り値: [id:41, id:40, id:39, id:38, id:37]

offset 方式との違い

offset(skip/take)cursor
ページ指定方法件数ベース(何件スキップ)レコード位置ベース(どのIDの次)
大量データの性能スキップ量が増えるほど遅くなる常に一定
総件数の取得容易(count と並列取得)困難(cursor 方式では totalPages が出せない)
UI との相性ページ番号 UI に向く無限スクロール・「次へ」ナビに向く
途中ページへの移動簡単(page=N で指定)困難(途中のカーソル値が必要)

向いている場面 / 向いていない場面

向いている場面

  • SNS フィードや無限スクロールで「次の N 件」だけ取得するとき
  • 数十万件以上のテーブルをページネーションするとき(offset の遅延を避けたい)
  • API のレスポンスとして nextCursor をクライアントに渡すとき

向いていない場面

  • 「3 ページ目に飛ぶ」など任意ページへのジャンプが必要な管理画面
  • 総件数・総ページ数を表示するページネーション UI
  • ユーザーが URL を直接編集してページ指定するケース

take / skip / orderBy との関係

// カーソルページネーションの典型パターン
const items = await prisma.post.findMany({
  take: limit + 1,       // 1 件多く取得して「次ページあり」を判定
  cursor: cursor ? { id: Number(cursor) } : undefined,
  skip: cursor ? 1 : 0, // cursor がある場合のみ skip: 1
  orderBy: { id: "desc" },
});

const hasNextPage = items.length > limit;
const nextCursor = hasNextPage ? String(items[limit - 1].id) : null;
  • take: limit + 1 で 1 件多く取得し、limit 超えなら次ページが存在すると判定する
  • orderBy は必須。ソート順が不定だとカーソル位置が意味をなさない
  • cursor に使うフィールドには DB インデックスが必要(通常 @id は自動でインデックス付き)

関連サンプルの見どころ

  • prisma-pagination-query: offset 方式と cursor 方式の両パターンを Route Handler で実装する例

offset 方式と cursor 方式の選び分けを比較でまとめたページもあります → offset vs cursor ページネーション

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例