使用 Prisma Optimize 進行查詢最佳化
本指南展示瞭如何識別和最佳化查詢效能、除錯效能問題以及應對常見的挑戰。
除錯效能問題
一些常見做法可能導致查詢變慢和效能問題,例如:
- 過度獲取資料
- 缺少索引
- 未快取重複查詢
- 執行全表掃描
有關更多潛在效能問題的原因,請訪問此頁面。
Prisma Optimize 提供建議,以識別和解決上述及更多低效率問題,從而幫助提高查詢效能。
要開始使用,請遵循整合指南,將 Prisma Optimize 新增到您的專案,開始診斷慢查詢。
您還可以在客戶端級別記錄查詢事件,以檢視生成的查詢、它們的引數和執行時間。
如果您特別關注查詢持續時間監控,請考慮使用日誌中介軟體。
使用批次查詢
通常,批次讀取和寫入大量資料效能更好——例如,分批插入 50,000 條記錄(每批 1000 條),而不是執行 50,000 次單獨插入。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(熱模組替換)框架的無伺服器開發環境,請確保您在開發中正確處理Prisma 的單一例項。
解決 N+1 問題
N+1 問題發生在您迴圈查詢結果並為每個結果執行一個額外查詢時,導致 n 個查詢加上原始查詢(N+1)。這是 ORM 的常見問題,特別是在與 GraphQL 結合使用時,因為您的程式碼生成效率低下的查詢並不總是顯而易見的。
使用 findUnique() 和 Prisma Client 的 dataloader 解決 GraphQL 中的 N+1 問題
如果以下條件成立,Prisma Client dataloader 會自動對在同一tick 中發生且具有相同 where 和 include 引數的 findUnique() 查詢進行批次處理:
where過濾器的所有條件都在您查詢的同一模型的標量欄位(唯一或非唯一)上。- 所有條件都使用
equal過濾器,無論是透過簡寫還是顯式語法(where: { field: <val>, field1: { equals: <val> } })。 - 沒有布林運算子或關係過濾器。
findUnique() 的自動批次處理在 GraphQL 上下文中特別有用。GraphQL 為每個欄位執行一個單獨的解析器函式,這使得最佳化巢狀查詢變得困難。
例如——以下 GraphQL 執行 allUsers 解析器以獲取所有使用者,並每個使用者一次執行 posts 解析器以獲取每個使用者的帖子(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 的解析器函式會每個使用者一次被呼叫。這導致每個使用者而不是單個 findMany() 返回所有使用者的所有帖子(展開 CLI 輸出以檢視查詢)的 findMany() 查詢 ✘。
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:使用 Fluent API 批次查詢
結合使用 findUnique() 和流暢 API (.posts()),如所示返回使用者的帖子。即使解析器每個使用者呼叫一次,Prisma Client 中的 Prisma dataloader ✔ 會批次處理 findUnique() 查詢。
使用 prisma.user.findUnique(...).posts() 查詢返回帖子而不是 prisma.posts.findMany() 可能看起來有悖常理——特別是前者會導致兩次查詢而不是一次。
您需要使用流暢 API (user.findUnique(...).posts()) 返回帖子的唯一原因是 Prisma Client 中的 dataloader 批次處理 findUnique() 查詢,但目前不會批次處理 findMany() 查詢。
當 dataloader 批次處理 findMany() 查詢或您的查詢將 relationStrategy 設定為 join 時,您不再需要以這種方式將 findUnique() 與流暢 API 一起使用。
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 解析器每個使用者呼叫一次,Prisma Client 中的 dataloader 會將具有相同引數和選擇集的 findUnique() 查詢分組。每個組都會被最佳化為一個單一的 findMany()。
解決方案 2:使用 JOIN 執行查詢
您可以透過將 relationLoadStrategy 設定為 "join" 來使用資料庫連線執行查詢,確保只對資料庫執行一次查詢。
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 查詢——一個 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 查詢——一個用於獲取使用者,另一個用於獲取帖子。這被稱為巢狀讀取。
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 列表中的所有帖子
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" 來使用資料庫連線執行查詢,確保只對資料庫執行一次查詢。
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,
},
},
})