ページネーション
Prisma Clientは、オフセットページネーションとカーソルベースのページネーションの両方をサポートしています。
オフセットページネーション
オフセットページネーションでは、skipとtakeを使用して、特定数の結果をスキップし、限定された範囲を選択します。次のクエリは最初の3件のPostレコードをスキップし、4件目から7件目のレコードを返します。
const results = await prisma.post.findMany({
skip: 3,
take: 4,
})

結果のページを実装するには、表示する1ページあたりの結果数にページ数を掛けた数をskipするだけです。
✔ オフセットページネーションの利点
- 任意のページにすぐにジャンプできます。例えば、200件のレコードを
skipし、10件をtakeすることで、結果セットの21ページ目に直接ジャンプするのをシミュレートできます(基盤となるSQLはOFFSETを使用します)。これはカーソルベースのページネーションでは不可能です。 - 同じ結果セットを任意のソート順でページネーションできます。例えば、名でソートされた
Userレコードのリストの21ページ目にジャンプできます。これはカーソルベースのページネーションでは不可能です。カーソルベースのページネーションは、一意で連続する列によるソートが必要です。
✘ オフセットページネーションの欠点
- オフセットページネーションはデータベースレベルで**スケーリングしません**。例えば、200,000件のレコードをスキップして最初の10件を取得する場合、データベースは要求された10件を返す前に、最初の200,000件のレコードを走査する必要があります。これはパフォーマンスに悪影響を及ぼします。
オフセットページネーションのユースケース
- 小規模な結果セットの浅いページネーション。例えば、
Postレコードを著者でフィルタリングし、結果をページネーションできるブログインターフェースなど。
例: フィルタリングとオフセットページネーション
次のクエリは、emailフィールドにprisma.ioを含むすべてのレコードを返します。このクエリは最初の40件のレコードをスキップし、41件目から50件目のレコードを返します。
const results = await prisma.post.findMany({
skip: 40,
take: 10,
where: {
email: {
contains: 'prisma.io',
},
},
})
例: ソートとオフセットページネーション
次のクエリは、emailフィールドにPrismaを含むすべてのレコードを返し、結果をtitleフィールドでソートします。このクエリは最初の200件のレコードをスキップし、201件目から220件目のレコードを返します。
const results = await prisma.post.findMany({
skip: 200,
take: 20,
where: {
email: {
contains: 'Prisma',
},
},
orderBy: {
title: 'desc',
},
})
カーソルベースのページネーション
カーソルベースのページネーションでは、cursorとtakeを使用して、指定された**カーソル**の前または後の限定された結果セットを返します。カーソルは結果セット内の位置をブックマークし、IDやタイムスタンプのような一意で連続したカラムである必要があります。
次の例は、"Prisma"という単語を含む最初の4件のPostレコードを返し、最後のレコードのIDをmyCursorとして保存します。
注: これは最初のクエリであるため、渡すカーソルはありません。
const firstQueryResults = await prisma.post.findMany({
take: 4,
where: {
title: {
contains: 'Prisma' /* Optional filter */,
},
},
orderBy: {
id: 'asc',
},
})
// Bookmark your location in the result set - in this
// case, the ID of the last post in the list of 4.
const lastPostInResults = firstQueryResults[3] // Remember: zero-based index! :)
const myCursor = lastPostInResults.id // Example: 29
次の図は、最初の4件の結果(または1ページ目)のIDを示しています。次のクエリのカーソルは**29**です。

2番目のクエリは、"Prisma"という単語を含む最初の4件のPostレコードを、**指定されたカーソルの後**(つまり、**29**より大きいID)に返します。
const secondQueryResults = await prisma.post.findMany({
take: 4,
skip: 1, // Skip the cursor
cursor: {
id: myCursor,
},
where: {
title: {
contains: 'Prisma' /* Optional filter */,
},
},
orderBy: {
id: 'asc',
},
})
const lastPostInResults = secondQueryResults[3] // Remember: zero-based index! :)
const myCursor = lastPostInResults.id // Example: 52
次の図は、ID **29**のレコード**の後**の最初の4件のPostレコードを示しています。この例では、新しいカーソルは**52**です。

FAQ
常にskip: 1を設定する必要がありますか?
skip: 1を設定しない場合、結果セットには以前のカーソルが含まれます。最初のクエリは4件の結果を返し、カーソルは**29**です。

skip: 1なしの場合、2番目のクエリはカーソルの後の4件の結果(カーソルを含む)を返します。

skip: 1を設定すると、カーソルは含まれません。

希望するページネーションの動作に応じて、skip: 1を設定するかどうかを選択できます。
カーソルの値を推測できますか?
次のカーソルの値を推測すると、結果セット内の不明な場所にページングされてしまいます。IDは連続していますが、特にフィルタリングされた結果セットでは、増分率(1、2、3よりも2、20、32など)を予測することはできません。
カーソルベースのページネーションは、基盤となるデータベースのカーソル概念を使用しますか?
いいえ、カーソルページネーションは基盤となるデータベースのカーソルを使用しません(例: PostgreSQL)。
カーソル値が存在しない場合はどうなりますか?
存在しないカーソルを使用するとnullが返されます。Prisma Clientは隣接する値を特定しようとしません。
✔ カーソルベースのページネーションの利点
- カーソルベースのページネーションは**スケーリングします**。基盤となるSQLは
OFFSETを使用せず、代わりにcursorの値より大きいIDを持つすべてのPostレコードをクエリします。
✘ カーソルベースのページネーションの欠点
- カーソルによってソートする必要があります。カーソルは一意で連続するカラムである必要があります。
- カーソルのみを使用して特定のページにジャンプすることはできません。例えば、1ページ目から399ページ目までを最初に要求することなく、どのカーソルが400ページ目(1ページあたり20件)の開始を表すかを正確に予測することはできません。
カーソルベースのページネーションのユースケース
- 無限スクロール - 例えば、ブログ記事を日付/時刻の降順でソートし、一度に10件ずつブログ記事を要求する。
- 結果セット全体をバッチでページングする - 例えば、長時間実行されるデータエクスポートの一部として。
例: フィルタリングとカーソルベースのページネーション
const secondQuery = await prisma.post.findMany({
take: 4,
cursor: {
id: myCursor,
},
where: {
title: {
contains: 'Prisma' /* Optional filter */,
},
},
orderBy: {
id: 'asc',
},
})
ソートとカーソルベースのページネーション
カーソルベースのページネーションでは、IDやタイムスタンプのような連続した一意のカラムでソートする必要があります。この値はカーソルとして知られ、結果セット内の場所をブックマークし、次のセットを要求できるようにします。
例: カーソルベースのページネーションで逆方向にページング
逆方向にページングするには、takeを負の値に設定します。次のクエリは、IDが200未満の4件のPostレコードを、カーソルを除外して返します。
const myOldCursor = 200
const firstQueryResults = await prisma.post.findMany({
take: -4,
skip: 1,
cursor: {
id: myOldCursor,
},
where: {
title: {
contains: 'Prisma' /* Optional filter */,
},
},
orderBy: {
id: 'asc',
},
})