メインコンテンツへスキップ

ページネーション

Prisma Clientは、オフセットページネーションとカーソルベースのページネーションの両方をサポートしています。

オフセットページネーション

オフセットページネーションは、skiptakeを使用して、特定の結果数をスキップし、制限された範囲を選択します。次のクエリは、最初の3つのPostレコードをスキップし、レコード4〜7を返します。

const results = await prisma.post.findMany({
skip: 3,
take: 4,
})

結果のページを実装するには、ページごとに表示する結果数にページ数を掛けた数をskipするだけです。

✔ オフセットページネーションの利点

  • 任意のページにすぐにジャンプできます。たとえば、skip 200レコードとtake 10を使用すると、結果セットの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',
},
})

カーソルベースのページネーション

カーソルベースのページネーションは、cursortakeを使用して、指定されたカーソルの前または後の制限された結果セットを返します。カーソルは、結果セット内の場所をブックマークし、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番目のクエリは、指定されたカーソルより後の(言い換えれば、29よりも大きいIDの)"Prisma"という単語を含む最初の4つのPostレコードを返します。

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はシーケンシャルですが、増分のレートを予測することはできません(特にフィルタリングされた結果セットでは、123よりも22032である可能性が高くなります)。

カーソルベースのページネーションは、基盤となるデータベースのカーソルの概念を使用していますか?

いいえ、カーソルページネーションは、基盤となるデータベースのカーソルを使用しません(例:PostgreSQL)。

カーソル値が存在しない場合はどうなりますか?

存在しないカーソルを使用すると、nullが返されます。Prisma Clientは、隣接する値を検索しようとはしません。

✔ カーソルベースのページネーションの利点

  • カーソルベースのページネーションはスケールします。基盤となるSQLはOFFSETを使用しませんが、代わりにcursorの値よりも大きいIDを持つすべてのPostレコードをクエリします。

✘ カーソルベースのページネーションの欠点

  • カーソルでソートする必要があります。カーソルは一意のシーケンシャル列である必要があります。
  • カーソルのみを使用して特定のページにジャンプすることはできません。たとえば、最初に1〜399ページをリクエストせずに、ページ400(ページサイズ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',
},
})