跳到主要内容

查询优化

本指南介绍如何识别和优化查询性能,调试性能问题,以及解决常见挑战。

调试性能问题

几种常见的做法可能导致查询缓慢和性能问题,例如

  • 过度获取数据
  • 缺少索引
  • 未缓存重复查询
  • 执行全表扫描
信息

有关性能问题的更多潜在原因,请访问此页面

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 结合使用时,因为您的代码正在生成低效查询并不总是立即显而易见的。

在 GraphQL 中使用 findUnique() 和 Prisma Client 的 dataloader 解决 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() 查询,而不是单个 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 批量处理查询

结合流畅 API (.posts()) 使用 findUnique(),如下所示返回用户的帖子。 即使解析器为每个用户调用一次,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,
},
},
})