N+1 查询:ORM 最典型的性能陷阱

取 100 条记录再逐条查关联,就是 101 次查询。修法只有三种:预加载、批量查、或者干脆用一次 JOIN。

N+1 的形状固定:先查出一批主记录,再对每一条单独查关联。N 条主记录就是 N+1 次往返。

const posts = await db.post.findMany();          // 1 次
for (const post of posts) {
  post.author = await db.user.findUnique({       // N 次
    where: { id: post.authorId },
  });
}

100 篇文章就是 101 次查询。单次查询很快,但网络往返的累积延迟会把页面拖到几秒。

三种修法

预加载(ORM 的 include / with)。 多数 ORM 能一条语句搞定:

const posts = await db.post.findMany({ include: { author: true } });

ORM 内部通常发两条查询(一条取文章、一条 WHERE id IN (...) 取作者),而不是 N+1 条。这是首选。

手动批量查询。 收集所有外键,一次查回来再在内存里拼接:

const posts = await db.post.findMany();
const ids = [...new Set(posts.map((p) => p.authorId))];
const authors = await db.user.findMany({ where: { id: { in: ids } } });
const byId = new Map(authors.map((a) => [a.id, a]));
for (const post of posts) post.author = byId.get(post.authorId);

JOIN。 关系简单且不需要嵌套结构时,一次查询最直接。代价是主记录会被关联表放大,需要去重。

怎么发现

N+1 在开发环境看不出来 —— 本地数据库没有网络延迟,100 次查询只要几毫秒。到了生产环境才暴露。

手段 做法
查询日志 打开 SQL 日志,看同一形状的语句是否重复出现
APM 统计单请求的查询次数,设阈值告警
ORM 钩子 在测试里断言「一个请求的查询数 ≤ K」

第三条最有效:把查询次数写成测试断言,N+1 会在 CI 阶段就被拦住,不用等到线上。

什么时候可以不管

  • 主记录数量有硬上限(比如一次最多 5 条)
  • 关联数据在缓存里,不产生查询
  • 这个接口本来就不在关键路径上

判断依据是实际的数据规模。列表接口如果有分页、每页 20 条,20+1 次查询通常可以接受 —— 前提是别再嵌套一层关联。

N+1 不慢在单条查询,慢在往返次数。发现靠查询计数,修复靠一次批量。

← 返回文章列表

评论

…