ページネーション
Prisma Clientは、オフセットページネーションとカーソルベースのページネーションの両方をサポートしています。
オフセットページネーション
オフセットページネーションは、skip
とtake
を使用して、特定の結果数をスキップし、制限された範囲を選択します。次のクエリは、最初の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',
},
})
カーソルベースのページネーション
カーソルベースのページネーションは、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番目のクエリは、指定されたカーソルより後の(言い換えれば、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はシーケンシャルですが、増分のレートを予測することはできません(特にフィルタリングされた結果セットでは、1
、2
、3
よりも2
、20
、32
である可能性が高くなります)。
カーソルベースのページネーションは、基盤となるデータベースのカーソルの概念を使用していますか?
いいえ、カーソルページネーションは、基盤となるデータベースのカーソルを使用しません(例: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',
},
})