Database 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 層完成關聯資料查詢:$lookupJOIN
db.users.aggregate([
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "userId",
as: "posts"
}
}
])
  1. 批次查詢並在應用層組裝:$inIN
// 步驟 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);
});
  1. ORM ODM Eager Loading

總結

  1. 避免在迴圈中對資料庫進行查詢
  2. 了解 ORM 如何下查詢:許多 ORM 預設使用 Lazy Loading 也可能生成有 N+1 問題的低效率查詢,通常也有對應的解決方案可以多翻閱文件了解相關設定才能使 ORM 生成較有效率的查詢。