Database query performance issue: N+1 problem

数据库查询性能问题:N+1 问题

前言

很早以前接触资料库就有听说过「N+1 问题」,不过一直没有写下笔记认真思考过一次,这次撰写问题成因与详细解方与图表。

N+1 问题是什么?

「N+1 问题」主要指在与资料库查询一次 1 次时,之后针对其 N 个项目的每一个再分别执行 N 次关联查询,这样的逻辑容易造成严重的效能问题。

  • 1:一次查询找出所有吻合特定条件的资料
  • N:根据 1 的结果,再针对每笔资料查询其关联资料

资料库应用程式资料库应用程式N+1 查询问题示例问题方式:产生 N+1 次查询循环处理每个用户...... 重复 7 次 ...总共:11 次查询 (1 + 10)更好的方式:只需 2 次查询总共:2 次查询1. SELECT * FROM users取得 10 个用户返回 10 个用户2. SELECT * FROM posts WHERE user_id=1返回用户 1 的文章3. SELECT * FROM posts WHERE user_id=2返回用户 2 的文章4. SELECT * FROM posts WHERE user_id=3返回用户 3 的文章11. SELECT * FROM posts WHERE user_id=10返回用户10的文章1. SELECT * FROM users返回 10 个用户2. SELECT * FROM postsWHERE user_id IN (1,2,3,...,10)返回所有相关文章

怎么解决 N+1 问题?

问题在于批次且大量的访问资料库,如果可以改成一次性索取必要资料就能解决该问题。

  1. 资料结构设计(去正规化)
  2. 在 DB 层完成关联资料查询:$lookupJOINDB 層完成關聯資料查詢:$lookupJOIN
db.users.aggregate([
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "userId",
as: "posts"
}
}
])
  1. 批次查询请在应用层组装:$onIN
// 步骤 1: 查询所有文章
const posts = await Post.find(); // 100 篇文章
// 步骤 2: 收集所有作者 ID
const userIds = [...new Set(posts.map(p => p.userId))];
// 步骤 3: 批次查询所有作者 (只执行 1 次)
const users = await User.find({ _id: { $in: userIds } });
// 步骤 4: 在应用层组装
const userMap = new Map(users.map(u => [u._id, u]));
posts.forEach(post => {
post.author = userMap.get(post.userId);
});
userMap.get(userId)
  1. ORM ODM Eager Loading

总结

  1. 避免在循环中对资料库进行查询
  2. 了解 ORM 如何下查询:许多 ORM 预设使用 Lazy Loading 也可能生成有 N+1 问题的低效率查询,通常也有对应的解决方案可以多翻阅文件了解相关设定才能使 ORM 生成较有效率的查询。