跳至主要內容

使用 Prisma Optimize 進行查詢最佳化

本指南展示瞭如何識別和最佳化查詢效能、除錯效能問題以及應對常見的挑戰。

除錯效能問題

一些常見做法可能導致查詢變慢和效能問題,例如:

  • 過度獲取資料
  • 缺少索引
  • 未快取重複查詢
  • 執行全表掃描
資訊

有關更多潛在效能問題的原因,請訪問此頁面

Prisma Optimize 提供建議,以識別和解決上述及更多低效率問題,從而幫助提高查詢效能。

要開始使用,請遵循整合指南,將 Prisma Optimize 新增到您的專案,開始診斷慢查詢。

提示

您還可以在客戶端級別記錄查詢事件,以檢視生成的查詢、它們的引數和執行時間。

如果您特別關注查詢持續時間監控,請考慮使用日誌中介軟體

使用批次查詢

通常,批次讀取和寫入大量資料效能更好——例如,分批插入 50,000 條記錄(每批 1000 條),而不是執行 50,000 次單獨插入。PrismaClient 支援以下批次查詢:

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

建立多個 PrismaClient 例項可能會耗盡您的資料庫連線池,尤其是在無伺服器或邊緣環境中,這可能會降低其他查詢的速度。在無伺服器挑戰中瞭解更多資訊。

對於具有傳統伺服器的應用程式,請例項化 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(熱模組替換)框架的無伺服器開發環境,請確保您在開發中正確處理Prisma 的單一例項

解決 N+1 問題

N+1 問題發生在您迴圈查詢結果並為每個結果執行一個額外查詢時,導致 n 個查詢加上原始查詢(N+1)。這是 ORM 的常見問題,特別是在與 GraphQL 結合使用時,因為您的程式碼生成效率低下的查詢並不總是顯而易見的。

使用 findUnique() 和 Prisma Client 的 dataloader 解決 GraphQL 中的 N+1 問題

如果以下條件成立,Prisma Client dataloader 會自動對在同一tick 中發生且具有相同 whereinclude 引數的 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 },
})
},
})
},
})
顯示CLI結果

解決方案 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()
},
})
},
})
顯示CLI結果

如果 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
})
顯示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 查詢——一個用於獲取使用者,另一個用於獲取帖子。這被稱為巢狀讀取

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" 來使用資料庫連線執行查詢,確保只對資料庫執行一次查詢。

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.