查询优化
本指南介绍如何识别和优化查询性能、调试性能问题以及解决常见挑战。
调试性能问题
一些常见的做法会导致查询缓慢和性能问题,例如
- 过度获取数据
- 缺少索引
- 未缓存重复查询
- 执行全表扫描
有关性能问题的更多潜在原因,请访问 此页面。
Prisma Optimize 提供 建议 来识别和解决上述以及更多效率低下问题,帮助提高查询性能。
要开始使用,请按照 集成指南 并将 Prisma Optimize 添加到你的项目中,开始诊断慢查询。
你还可以 在客户端级别记录查询事件 以查看生成的查询、其参数和执行时间。
如果你特别关注监控查询持续时间,请考虑使用 日志记录中间件。
使用批量查询
通常,批量读取和写入大量数据效率更高 - 例如,将 50,000
条记录分批插入 1000
条,而不是作为 50,000
个单独的插入操作。PrismaClient
支持以下批量查询
重用 PrismaClient
或使用连接池以避免数据库连接池耗尽
创建多个 PrismaClient
实例可能会耗尽你的数据库连接池,尤其是在无服务器或边缘环境中,可能会减慢其他查询的速度。在 无服务器挑战 中了解更多信息。
对于具有传统服务器的应用程序,请实例化一次 PrismaClient
并在整个应用程序中重用它,而不是创建多个实例。例如,而不是
async function getPosts() {
const prisma = new PrismaClient()
await prisma.post.findMany()
}
async function getUsers() {
const prisma = new PrismaClient()
await prisma.user.findMany()
}
在专用文件中定义单个 PrismaClient
实例并重新导出它以供重用
export const prisma = new PrismaClient()
然后导入共享实例
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 的数据加载器在 GraphQL 中解决 N+1
如果满足以下条件,Prisma Client 数据加载器会自动批处理在同一 tick 中发生且具有相同的 where
和 include
参数的 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 },
})
},
})
},
})
解决方案 1:使用 Fluent API 批处理查询
如所示,结合使用 findUnique()
和 Fluent API(.posts()
)来返回用户的帖子。即使解析器为每个用户调用一次,Prisma Client 中的数据加载器也会✔ 批处理 findUnique()
查询。
使用 prisma.user.findUnique(...).posts()
查询返回帖子而不是 prisma.posts.findMany()
似乎违反直觉 - 特别是前者会导致两个查询而不是一个查询。
你唯一需要使用 Fluent API(user.findUnique(...).posts()
)返回帖子的原因是 Prisma Client 中的数据加载器会批处理 findUnique()
查询,并且目前不会 批处理 findMany()
查询。
当数据加载器批处理 findMany()
查询或你的查询的 relationStrategy
设置为 join
时,你不再需要以这种方式使用 findUnique()
和 Fluent 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()
},
})
},
})
如果为每个用户调用一次 posts
解析器,则 Prisma Client 中的数据加载器会对具有相同参数和选择集的 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
})
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
过滤器 - 将
relationLoadStrategy
设置为"join"
使用 include
解决 N+1 问题
您可以使用 include
返回每个用户的帖子。这只会导致 **两个** SQL 查询 - 一个获取用户,另一个获取帖子。这被称为 嵌套读取。
const usersWithPosts = await prisma.user.findMany({
include: {
posts: true,
},
})
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,
},
},
})
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,
},
},
})