查詢最佳化
本指南說明如何識別和最佳化查詢效能、偵錯效能問題,以及解決常見挑戰。
偵錯效能問題
幾種常見的做法可能會導致查詢速度變慢和效能問題,例如
- 過度提取資料
- 遺失索引
- 未快取重複查詢
- 執行完整表格掃描
如需更多可能導致效能問題的原因,請造訪此頁面。
Prisma Optimize 提供建議,以識別和解決上述及更多的效率低落問題,協助提升查詢效能。
若要開始使用,請依照整合指南,將 Prisma Optimize 新增至您的專案,開始診斷慢速查詢。
您也可以在用戶端層級記錄查詢事件,以檢視產生的查詢、其參數和執行時間。
如果您特別關注監控查詢持續時間,請考慮使用記錄中介軟體。
使用大量查詢
以大量方式讀取和寫入大量資料通常效能更高,例如,以 1000
個批次插入 50,000
筆記錄,而不是 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 結合使用時,因為您的程式碼是否產生效率低落的查詢並不總是顯而易見。
在 GraphQL 中使用 findUnique()
和 Prisma Client 的資料載入器解決 n+1 問題
Prisma Client 資料載入器會自動批次處理在同一個 tick 中發生的 findUnique()
查詢,並具有相同的 where
和 include
參數,如果
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()
查詢 ✘,而不是單一 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 批次處理查詢
結合使用 findUnique()
與流暢 API (.posts()
),如所示範,以傳回使用者的貼文。即使解析器針對每個使用者調用一次,Prisma Client 中的 Prisma 資料載入器✔ 批次處理 findUnique()
查詢。
使用 prisma.user.findUnique(...).posts()
查詢傳回貼文,而不是 prisma.posts.findMany()
,似乎違反直覺,特別是前者會導致兩個查詢,而不是一個。
您需要使用流暢 API (user.findUnique(...).posts()
) 傳回貼文的唯一原因是 Prisma Client 中的資料載入器批次處理 findUnique()
查詢,並且目前不批次處理 findMany()
查詢。
當資料載入器批次處理 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 中的資料載入器會將具有相同參數和選取集的 findUnique()
查詢分組。每個群組都會最佳化為單一 findMany()
。
解決方案 2:使用 JOIN 執行查詢
您可以透過將 relationLoadStrategy
設定為 "join"
,使用資料庫 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"
,使用資料庫 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,
},
},
})