跳至主要內容

使用 Prisma Optimize 進行查詢優化

本指南說明如何識別並優化查詢效能、調試效能問題,以及解決常見挑戰。

調試效能問題

幾種常見的做法可能導致查詢緩慢和效能問題,例如:

  • 過度獲取資料 (Over-fetching data)
  • 缺少索引
  • 未對重複查詢進行快取
  • 執行全表掃描
資訊

如需更多導致效能問題的潛在原因,請訪問此頁面

Prisma Optimize 提供建議,以識別並解決上述及更多的效率低下問題,從而協助改善查詢效能。

若要開始使用,請依照整合指南操作,將 Prisma Optimize 加入您的專案以開始診斷緩慢的查詢。

提示

您也可以在用戶端層級記錄查詢事件,以查看產生的查詢、其參數及執行時間。

使用批次查詢 (Bulk queries)

通常,以批次方式讀取和寫入大量資料會更有效率——例如,以 1000 筆為一批次插入 50,000 筆記錄,而不是進行 50,000 次獨立的插入。PrismaClient 支援以下批次查詢:

重複使用 PrismaClient 或使用連線池以避免資料庫連線池耗盡

建立多個 PrismaClient 執行個體可能會耗盡您的資料庫連線池,尤其是在 Serverless 或 Edge 環境中,這可能會拖慢其他查詢。在無伺服器挑戰 (serverless challenge) 中了解更多資訊。

對於使用傳統伺服器的應用程式,請將 PrismaClient 執行個體化一次並在整個應用程式中重複使用,而不是建立多個執行個體。例如,不要這樣做:

query.ts
async function getPosts() {
const prisma = new PrismaClient()
await prisma.post.findMany()
}

async function getUsers() {
const prisma = new PrismaClient()
await prisma.user.findMany()
}

而是在專用的檔案中定義一個單一的 PrismaClient 執行個體,並重新匯出以供重複使用:

db.ts
export const prisma = new PrismaClient()

然後匯入共用的執行個體:

query.ts
import { prisma } from "db.ts"

async function getPosts() {
await prisma.post.findMany()
}

async function getUsers() {
await prisma.user.findMany()
}

對於使用 HMR (熱模組替換) 框架的 Serverless 開發環境,請確保您在開發過程中正確處理單一 Prisma 執行個體

解決 n+1 問題

n+1 問題發生在您遍歷查詢結果並對**每個結果**執行一次額外查詢時,導致執行了 n 次查詢加上原始的那一次 (n+1)。這在 ORM 中是一個常見問題,特別是在與 GraphQL 結合使用時,因為您的程式碼產生的低效查詢並不總是顯而易見的。

使用 findUnique() 和 Prisma Client 的 Dataloader 在 GraphQL 中解決 n+1 問題

Prisma Client Dataloader 會自動將在同一個 tick 中發生,且具有相同 whereinclude 參數的 findUnique() 查詢進行批次處理 (batching),前提是:

  • where 過濾器的所有條件都位於您正在查詢的同一個模型的純量欄位(唯一或非唯一)上。
  • 所有條件都使用 equal 過濾器,無論是透過簡寫還是顯式語法 (where: { field: <val>, field1: { equals: <val> } })
  • 不存在布林運算子或關聯過濾器。

自動批次處理 findUnique()GraphQL 環境中特別有用。GraphQL 會為每個欄位執行獨立的解析器函數 (resolver),這使得優化巢狀查詢變得困難。

例如——以下 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 },
})
},
})
},
})
顯示CLI結果

解決方案 1:使用 Fluent API 進行批次查詢

如圖所示,結合使用 findUnique()Fluent API (.posts()) 來回傳使用者的貼文。即使解析器是為每位使用者呼叫一次,Prisma Client 中的 Prisma Dataloader 也會 ✔ 對 findUnique() 查詢進行批次處理

資訊

使用 prisma.user.findUnique(...).posts() 查詢來回傳貼文,而不是使用 prisma.posts.findMany(),這看起來可能違反直覺——特別是因為前者會產生兩次查詢而不是一次。

您需要使用 Fluent API (user.findUnique(...).posts()) 來回傳貼文的唯一原因是,Prisma Client 中的 Dataloader 會批次處理 findUnique() 查詢,目前並未 批次處理 findMany() 查詢

當 Dataloader 批次處理 findMany() 查詢,或者您的查詢將 relationStrategy 設定為 join 時,您就不再需要以這種方式使用帶有 Fluent 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()
},
})
},
})
顯示CLI結果

如果 posts 解析器是為每位使用者呼叫一次,Prisma Client 中的 Dataloader 會將具有相同參數和選取集 (selection set) 的 findUnique() 查詢進行分組。每組都會被最佳化為單一的 findMany()

解決方案 2:使用 JOINs 執行查詢

您可以透過將 relationLoadStrategy 設定為 "join" 來使用 資料庫聯接 (database 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
})
顯示CLI結果
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 解決 n+1 問題

您可以使用 include 來回傳每位使用者的貼文。這只會導致兩次 SQL 查詢——一次用於取得使用者,一次用於取得貼文。這稱為 巢狀讀取 (nested read)

const usersWithPosts = await prisma.user.findMany({
include: {
posts: true,
},
})
顯示CLI結果
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,
},
},
})
顯示CLI結果
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" 來使用 資料庫聯接 (database 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,
},
},
})
© . This site is unofficial and not affiliated with Prisma Data, Inc.