跳到主要內容

查詢最佳化

本指南說明如何識別和最佳化查詢效能、偵錯效能問題,以及解決常見挑戰。

偵錯效能問題

幾種常見的做法可能會導致查詢速度變慢和效能問題,例如

  • 過度提取資料
  • 遺失索引
  • 未快取重複查詢
  • 執行完整表格掃描
資訊

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

Prisma Optimize 提供建議,以識別和解決上述及更多的效率低落問題,協助提升查詢效能。

若要開始使用,請依照整合指南,將 Prisma Optimize 新增至您的專案,開始診斷慢速查詢。

提示

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

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

使用大量查詢

以大量方式讀取和寫入大量資料通常效能更高,例如,以 1000 個批次插入 50,000 筆記錄,而不是 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 結合使用時,因為您的程式碼是否產生效率低落的查詢並不總是顯而易見。

在 GraphQL 中使用 findUnique() 和 Prisma Client 的資料載入器解決 n+1 問題

Prisma Client 資料載入器會自動批次處理在同一個 tick 中發生的 findUnique() 查詢,並具有相同的 whereinclude 參數,如果

  • 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 },
})
},
})
},
})
顯示CLI結果

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

如果 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
})
顯示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",使用資料庫 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,
},
},
})