跳过至主要内容

查询优化

本指南介绍了优化查询性能、调试性能问题以及如何解决常见的性能问题(如 n+1 问题)的方法。

提示

要调试慢速查询,您可以使用 Prisma Optimize 并遵循提供的 建议 来提高应用程序中的查询性能。

调试性能问题

为了帮助您调试和诊断性能问题,您可以 在客户端级别记录查询事件,这使您能够查看生成的查询、参数和持续时间。

或者,如果您只对运行查询所需的时间感兴趣,则可以实现 日志记录中间件

解决 n+1 问题

当您循环浏览查询结果并对 **每个结果** 执行一个额外的查询时,就会出现 n+1 问题,这会导致 n 个查询加上原始查询 (n+1)。这是 ORM 中的一个常见问题,尤其是在与 GraphQL 结合使用时,因为并非总是立即清楚您的代码正在生成低效的查询。

使用 findUnique() 和 Prisma 客户端的数据加载器在 GraphQL 中解决 n+1

Prisma 客户端的数据加载器会自动将发生在同一 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结果

相反,请使用 findUnique() 结合 流畅 API.posts())(如所示)来返回用户的帖子。即使解析器为每个用户调用一次,Prisma 客户端中的 Prisma 数据加载器也会 **✔ 将 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 客户端中的数据加载器会对具有相同参数和选择集的 findUnique() 查询进行分组。每个组都被优化为一个单独的 findMany()

我是否必须使用流畅 API 来启用查询批处理?

使用 prisma.user.findUnique(...).posts() 查询来返回帖子而不是 prisma.posts.findMany() 可能看起来违反直觉 - 特别是前者会导致两个查询而不是一个查询。

您需要使用流畅 API (user.findUnique(...).posts()) 来返回帖子的 **唯一** 原因是 Prisma 客户端中的数据加载器会将 findUnique() 查询批处理,并且目前 不会将 findMany() 查询批处理

当数据加载器将 findMany() 查询批处理时,您不再需要以这种方式使用带流畅 API 的 findUnique()

其他上下文中的 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)来返回用户和相关的帖子
  • 使用 in 过滤器

使用 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

使用批量查询

通常,以批量方式读取和写入大量数据会更高效 - 例如,以 1000 个批次插入 50,000 条记录,而不是作为 50,000 个单独的插入操作。Prisma 客户端支持以下批量查询

使用 select 来限制返回的列数

使用 select 来限制返回的列数 **不太可能对性能产生影响**,除非您已通过测试确定这是性能瓶颈。例如,如果您的情况是以下情况,读取所有字段可能会对性能产生负面影响:

  • 具有大量列的表
  • 存储在磁盘上的不同位置而不是行的较大的列,这会导致额外的磁盘读取

此外,如果您有一个成熟的产品,具有完善的查询模式和经过精心调整的索引,则选择特定列子集可能会有益,因为它可以避免从磁盘读取数据。但是,在大多数情况下,只有在达到一定规模时才需要进行这种级别的性能调整。