Prisma Optimize を使用したクエリの最適化
このガイドでは、クエリパフォーマンスの特定と最適化、パフォーマンス問題のデバッグ、一般的な課題への対処方法を説明します。
パフォーマンス問題のデバッグ
以下のような一般的な習慣が、クエリの低速化やパフォーマンス問題を引き起こす可能性があります。
- データの過剰取得
- インデックスの欠落
- 繰り返しクエリのキャッシュなし
- フルテーブルスキャンを実行している
パフォーマンス問題の潜在的な原因については、こちらのページをご覧ください。
Prisma Optimizeは、上記の非効率性などを特定して対処するための推奨事項を提供し、クエリパフォーマンスの向上を支援します。
始めるには、統合ガイドに従ってPrisma Optimizeをプロジェクトに追加し、遅いクエリの診断を開始してください。
生成されたクエリ、そのパラメータ、実行時間を確認するには、クライアントレベルでクエリイベントをログ出力することもできます。
特にクエリの実行時間の監視に重点を置く場合は、ロギングミドルウェアの使用を検討してください。
バルククエリの使用
大量のデータをバルクで読み書きする方が、一般的にパフォーマンスが向上します。例えば、50,000
件のレコードを50,000
回の個別挿入ではなく、1000
件ずつのバッチで挿入するといった形です。PrismaClient
は以下のバルククエリをサポートしています
PrismaClient
の再利用またはコネクションプーリングの使用により、データベースコネクションプールの枯渇を回避する
PrismaClient
の複数のインスタンスを作成すると、特にサーバーレスまたはエッジ環境において、データベース接続プールが枯渇し、他のクエリの速度が低下する可能性があります。サーバーレスの課題で詳細をご覧ください。
従来のサーバーを持つアプリケーションでは、PrismaClient
を一度インスタンス化し、複数のインスタンスを作成するのではなく、アプリケーション全体で再利用してください。例えば、以下の代わりに
async function getPosts() {
const prisma = new PrismaClient()
await prisma.post.findMany()
}
async function getUsers() {
const prisma = new PrismaClient()
await prisma.user.findMany()
}
専用ファイルで単一のPrismaClient
インスタンスを定義し、再利用のためにエクスポートします。
export const prisma = new PrismaClient()
次に、共有インスタンスをインポートします。
import { prisma } from "db.ts"
async function getPosts() {
await prisma.post.findMany()
}
async function getUsers() {
await prisma.user.findMany()
}
HMR (Hot Module Replacement) を使用するフレームワークを持つサーバーレス開発環境では、開発環境でのPrismaの単一インスタンスを適切に処理してください。
N+1問題の解決
N+1問題は、クエリの結果をループ処理し、各結果に対して追加のクエリを1つ実行することで発生し、元のクエリに加えてn
個のクエリ(n+1)が実行されます。これはORM、特にGraphQLとの組み合わせでよく見られる問題です。なぜなら、コードが非効率なクエリを生成していることが常にすぐに明らかになるわけではないからです。
findUnique()
とPrisma Clientのデータローダーを使ったGraphQLにおけるN+1の解決
Prisma Clientのデータローダーは、同じティックで発生し、where
とinclude
パラメータが同じであるfindUnique()
クエリを自動的にバッチ処理します。
where
フィルターのすべての条件が、クエリ対象の同じモデルのスカラーフィールド(ユニークまたは非ユニーク)に基づいている場合。- すべての条件が、省略形または明示的な構文
(where: { field: <val>, field1: { equals: <val> } })
のいずれかを通じてequal
フィルターを使用している場合。 - 論理演算子やリレーションフィルターが存在しない場合。
findUnique()
の自動バッチ処理は、GraphQLコンテキストで特に役立ちます。GraphQLはフィールドごとに個別のリゾルバー関数を実行するため、ネストされたクエリの最適化が難しくなることがあります。
例えば、以下のGraphQLは、allUsers
リゾルバーを実行してすべてのユーザーを取得し、posts
リゾルバーをユーザーごとに1回実行して各ユーザーの投稿を取得します(N+1)。
query {
allUsers {
id,
posts {
id
}
}
}
allUsers
クエリはuser.findMany(..)
を使用してすべてのユーザーを返します
const Query = objectType({
name: 'Query',
definition(t) {
t.nonNull.list.nonNull.field('allUsers', {
type: 'User',
resolve: (_parent, _args, context) => {
return context.prisma.user.findMany()
},
})
},
})
これにより、単一のSQLクエリが実行されます
{
timestamp: 2021-02-19T09:43:06.332Z,
query: 'SELECT `dev`.`User`.`id`, `dev`.`User`.`email`, `dev`.`User`.`name` FROM `dev`.`User` WHERE 1=1 LIMIT ? OFFSET ?',
params: '[-1,0]',
duration: 0,
target: 'quaint::connector::metrics'
}
しかし、posts
のリゾルバー関数はユーザーごとに1回呼び出されます。これにより、すべてのユーザーのすべての投稿を返す単一のfindMany()
ではなく、ユーザーごとに✘1つのfindMany()
クエリが実行されます(クエリを確認するにはCLI出力を展開してください)。
const User = objectType({
name: 'User',
definition(t) {
t.nonNull.int('id')
t.string('name')
t.nonNull.string('email')
t.nonNull.list.nonNull.field('posts', {
type: 'Post',
resolve: (parent, _, context) => {
return context.prisma.post.findMany({
where: { authorId: parent.id || undefined },
})
},
})
},
})
解決策1:フルーエントAPIによるクエリのバッチ処理
ユーザーの投稿を返すには、示されているようにフルーエントAPI(.posts()
)と組み合わせてfindUnique()
を使用します。リゾルバーはユーザーごとに1回呼び出されますが、Prisma ClientのPrismaデータローダーは✔ findUnique()
クエリをバッチ処理します。
投稿を返すのにprisma.posts.findMany()
ではなくprisma.user.findUnique(...).posts()
クエリを使用するのは、直感に反するように思えるかもしれません。特に、前者が1つではなく2つのクエリを生成するからです。
投稿を返すためにフルーエントAPI(user.findUnique(...).posts()
)を使用する必要がある唯一の理由は、Prisma ClientのデータローダーがfindUnique()
クエリをバッチ処理する一方で、現在のところfindMany()
クエリをバッチ処理しないためです。
データローダーがfindMany()
クエリをバッチ処理するか、クエリのrelationStrategy
がjoin
に設定されている場合、このようにフルーエントAPIでfindUnique()
を使用する必要はなくなります。
const User = objectType({
name: 'User',
definition(t) {
t.nonNull.int('id')
t.string('name')
t.nonNull.string('email')
t.nonNull.list.nonNull.field('posts', {
type: 'Post',
resolve: (parent, _, context) => {
return context.prisma.post.findMany({
where: { authorId: parent.id || undefined },
})
return context.prisma.user
.findUnique({
where: { id: parent.id || undefined },
})
.posts()
},
})
},
})
posts
リゾルバーがユーザーごとに1回呼び出される場合、Prisma Clientのデータローダーは、同じパラメータと選択セットを持つfindUnique()
クエリをグループ化します。各グループは単一のfindMany()
に最適化されます。
解決策2:JOINを使用したクエリの実行
relationLoadStrategy
を"join"
に設定することで、データベース結合を使用してクエリを実行し、データベースに対して1つだけのクエリが実行されるようにすることができます。
const User = objectType({
name: 'User',
definition(t) {
t.nonNull.int('id')
t.string('name')
t.nonNull.string('email')
t.nonNull.list.nonNull.field('posts', {
type: 'Post',
resolve: (parent, _, context) => {
return context.prisma.post.findMany({
relationLoadStrategy: "join",
where: { authorId: parent.id || undefined },
})
},
})
},
})
その他のコンテキストにおけるN+1
N+1問題は、複数のリゾルバーにまたがる単一のクエリを最適化する方法を見つける必要があるため、GraphQLコンテキストで最もよく見られます。しかし、独自のコードでforEach
を使って結果をループするだけでも、簡単にN+1問題を発生させることができます。
以下のコードはN+1クエリを生成します。すべてのユーザーを取得するための1つのfindMany()
と、各ユーザーの投稿を取得するためのユーザーごとのfindMany()
です。
// One query to get all users
const users = await prisma.user.findMany({})
// One query PER USER to get all posts
users.forEach(async (usr) => {
const posts = await prisma.post.findMany({
where: {
authorId: usr.id,
},
})
// Do something with each users' posts
})
SELECT "public"."User"."id", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1
SELECT "public"."Post"."id", "public"."Post"."title" FROM "public"."Post" WHERE "public"."Post"."authorId" = $1 OFFSET $2
SELECT "public"."Post"."id", "public"."Post"."title" FROM "public"."Post" WHERE "public"."Post"."authorId" = $1 OFFSET $2
SELECT "public"."Post"."id", "public"."Post"."title" FROM "public"."Post" WHERE "public"."Post"."authorId" = $1 OFFSET $2
SELECT "public"."Post"."id", "public"."Post"."title" FROM "public"."Post" WHERE "public"."Post"."authorId" = $1 OFFSET $2
/* ..and so on .. */
これは効率的なクエリ方法ではありません。代わりに、次の方法が考えられます。
- ネストされた読み取り(
include
)を使用して、ユーザーと関連する投稿を返します。 in
フィルターを使用します。relationLoadStrategy
を"join"
に設定します。
include
を使ったN+1の解決
include
を使用して各ユーザーの投稿を返すことができます。これにより、SQLクエリはユーザーを取得するための1つと投稿を取得するための1つの合計2つのみとなります。これはネストされた読み取りとして知られています。
const usersWithPosts = await prisma.user.findMany({
include: {
posts: true,
},
})
SELECT "public"."User"."id", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1
SELECT "public"."Post"."id", "public"."Post"."title", "public"."Post"."authorId" FROM "public"."Post" WHERE "public"."Post"."authorId" IN ($1,$2,$3,$4) OFFSET $5
in
を使ったN+1の解決
ユーザーIDのリストがある場合、in
フィルターを使用して、authorId
がそのIDリストにin
するすべての投稿を返すことができます。
const users = await prisma.user.findMany({})
const userIds = users.map((x) => x.id)
const posts = await prisma.post.findMany({
where: {
authorId: {
in: userIds,
},
},
})
SELECT "public"."User"."id", "public"."User"."email", "public"."User"."name" FROM "public"."User" WHERE 1=1 OFFSET $1
SELECT "public"."Post"."id", "public"."Post"."createdAt", "public"."Post"."updatedAt", "public"."Post"."title", "public"."Post"."content", "public"."Post"."published", "public"."Post"."authorId" FROM "public"."Post" WHERE "public"."Post"."authorId" IN ($1,$2,$3,$4) OFFSET $5
relationLoadStrategy: "join"
を使ったN+1の解決
relationLoadStrategy
を"join"
に設定することで、データベース結合を使用してクエリを実行し、データベースに対して1つだけのクエリが実行されるようにすることができます。
const users = await prisma.user.findMany({})
const userIds = users.map((x) => x.id)
const posts = await prisma.post.findMany({
relationLoadStrategy: "join",
where: {
authorId: {
in: userIds,
},
},
})