性能优化:连接池、查询优化、避免常见陷阱
连接池
原理
Prisma Client 默认建立的连接池大小取决于你用的数据库驱动。连接池的目的是复用数据库连接,而不是每次查询都新建一个 TCP 连接。
数据库 URL 里控制
连接池大小主要由 DATABASE_URL 里的参数控制(Prisma Client 构造函数不直接暴露这个参数):
postgresql://user:pass@host:5432/mydb?connection_limit=10&pool_timeout=20
| 参数 | 含义 | 默认 |
|---|---|---|
connection_limit | 最大连接数 | PostgreSQL 默认取决于数据库 |
pool_timeout | 获取新连接的超时(秒) | 10 |
socket_timeout | 单个查询超时(秒) | 无 |
推荐配置
- 开发环境:
connection_limit=5足够 - 生产长驻服务:
connection_limit=10~20,不要超过数据库的最大连接数 - Serverless:用 Accelerate 或 PgBouncer 连接池——Serverless 函数每次冷启动会新建连接,直接连数据库很容易把连接数耗尽
查看当前连接数
PostgreSQL 可以直接查:
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
如果你的连接数经常接近上限,要么调大 connection_limit,要么加独立连接池(Accelerate / PgBouncer)。
查询优化
1. 用 select 代替 include + 只取需要的字段
// ❌ 不好:include 会拉回所有普通字段 + 所有关联
const users = await prisma.user.findMany({
include: { posts: true },
})
// ✅ 好:select 只取需要的字段
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
posts: { select: { id: true, title: true } },
},
})
2. 不要用 include 做计数
// ❌ 不好:拉回所有 Post 记录只为了算长度
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
})
const postCount = user.posts.length
// ✅ 好:用 _count
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { _count: { posts: true } },
})
const postCount = user._count.posts
3. 避免 N+1
Prisma 自动帮你处理了大部分场景。唯一的陷阱是在循环里单独查询:
// ❌ 不好:N+1
const users = await prisma.user.findMany()
for (const user of users) {
const posts = await prisma.post.findMany({ where: { authorId: user.id } })
// 每个用户一次查询
}
// ✅ 好:一次 include 拉回
const users = await prisma.user.findMany({ include: { posts: true } })
4. 复杂查询用事务 + 批量
如果你需要创建多条记录,用 createMany 或 $transaction 数组模式:
// ✅ 好:批量创建
await prisma.post.createMany({
data: posts,
})
// ✅ 好:事务数组模式(一次事务里发多条 SQL)
await prisma.$transaction([
prisma.post.create({ data: post1 }),
prisma.post.create({ data: post2 }),
prisma.post.create({ data: post3 }),
])
// ❌ 不好:循环里逐条创建(每条都是独立事务)
for (const post of posts) {
await prisma.post.create({ data: post })
}
5. 原始 SQL 的坑
$queryRaw 和 $executeRaw 本身不是性能问题(它们就是直接发 SQL)。但:
- 不要把 Prisma Client 完全可以搞定的查询写成 raw——失去类型安全和关系推断
- raw 查询结果里的字段名是数据库层的——snake_case 还是 camelCase 取决于数据库,Prisma 不会帮你转换
- 避免在热路径上反复执行复杂的 CTE / 窗口函数查询——考虑缓存
事务超时
交互事务(传函数的 $transaction(async (tx) => {...}))默认 5 秒超时。事务里做慢操作(查第三方 API、处理大数组)会导致超时失败。
原则:事务里只做数据库操作,其他操作移到事务外。如果事务里的查询本身很慢,要么调大 timeout,要么优化查询本身。
await prisma.$transaction(async (tx) => {
// 只做数据库操作
}, {
maxWait: 5000, // 等连接的超时
timeout: 30000, // 事务本身的超时 → 调大
})
生产环境关掉 query 日志
// ❌ 生产环境不要这样
export const prisma = new PrismaClient({
log: ['query', 'error', 'warn'],
})
// ✅ 生产环境只记 error
export const prisma = new PrismaClient({
log: ['error'],
})
log: ['query'] 会把每条 SQL 都打到 stdout。这不仅污染日志,还增加了序列化开销——高 QPS 下会很可观。
Prisma 内部的性能改进
Prisma 7 有显著的性能改进(官方 benchmark):
- 类型检查快 70%(TypeScript 编辑器体验提升)
- 减少了 98% 需要评估的类型数量
- 生成的客户端引擎更轻量
如果还在用 Prisma 5 或更早,考虑升级到 7。
监控 Prisma 查询耗时
开发环境下 log: ['query'] 直接看。生产环境推荐用 Client Extension:
export const prisma = new PrismaClient().$extends({
query: {
async $allOperations({ model, operation, args, query }) {
const start = Date.now()
const result = await query(args)
const duration = Date.now() - start
if (duration > 100) {
console.warn(`[SLOW DB] ${model}.${operation} took ${duration}ms`, JSON.stringify(args))
}
return result
},
},
})
这样慢查询(超过 100ms)会被记录下来,方便定位。