前端進階

-

N + 1 問題是什麼?後端查詢的必要知識

this.web

文章封面

什麼是 N+1 問題?

N+1 問題是後端領域中非常常見、也非常重要的效能議題。

假設我們現在要查 10 篇貼文的內容以及前 5 則留言。

你會怎麼做呢?

如果我們先查一次主資料

SELECT * FROM posts LIMIT 10;

然後再用迴圈跑 10 次,每次再查留言:

SELECT * FROM comments WHERE post_id = ? LIMIT 5;

這樣總共會執行 11 次查詢1 次查主資料,外加 10 次查留言)。

當資料量很小時,看起來好像還可以接受,但如果查詢量變大,例如需要查詢 10,000 篇貼文,那就會造成 10,001 次查詢,效能會呈線性成長,甚至造成資料庫負載暴增

而且現實場景通常更複雜,例如我們還要查每個留言的作者,又需要再跑一次迴圈:

SELECT * FROM users WHERE id = ?;

總共會經歷:

  • 1 次查貼文
  • 10 次查留言
  • 50 次查留言的作者

一不小心就會變成 N x M 問題了。

而且這很容易不小心出現在有使用 ORM 的情況,因爲 ORM 的設計理念就是讓我們像操作物件一樣的去操作資料,所以會把查詢包裝得像普通函式呼叫,大家就會比較容易忽略查詢帶來的花費。

比如上面的例子,如果用 Prisma ORM 來寫就會類似這樣:

const posts = await prisma.post.findMany();

for (const post of posts) {
  const comments = await prisma.comment.findMany({
    where: { postId: post.id }
  });
  
  for (const comment of comments) {
    const user = await prisma.user.findUnique({ 
      where: { id: comment.userId }
    });
  }
}

如何解決 N + 1 問題?

核心方法其實非常單純,就是將分散的查詢合併為少數幾次查詢

我們可以藉由以下方式做到:

  • 透過資料庫的 JOIN
  • 使用 IN 查詢一次帶回所有需要的資料
  • 在 ORM 中使用 eager loading(例如 Prisma 的 include 或 select)

透過資料庫的 JOIN

當需要同時查詢多張資料表時,可以用 JOIN 一次把所有資料組合起來,避免一筆筆查詢關聯資料。

SELECT
  p.*,
  c.*
FROM posts p
LEFT JOIN comments c ON c.post_id = p.id
WHERE p.id IN (SELECT id FROM posts LIMIT 10);

這樣只需要一次查詢,就能取得貼文與留言的完整資料。

使用 IN 查詢的最佳化

如果只需要查某一張表(例如留言),可以利用 IN 一次查回所有相關資料,而不是為每個 post 個別執行查詢。

SELECT * FROM comments WHERE post_id IN (SELECT id FROM posts LIMIT 10);

簡單來說:

  • JOIN:需要同時查詢多張資料表的內容
  • WHERE IN:只需要查單一資料表,但需要以一組條件批次查詢

在 ORM 中使用 eager loading

eager loading 指的是在查詢主資料時,同時主動載入關聯資料,避免在迴圈中對 ORM 呼叫關聯屬性時才額外觸發查詢。

許多 ORM 預設採用 Lazy Loading,也就是遇到關聯資料時才會在背後發出查詢,這往往是造成 N+1 問題的主因。

以 Prisma 為例,可以利用 include 同步取回所有需要的資料:

const posts = await prisma.post.findMany({
  take: 10,
  include: {
    comments: true
  }
});

這樣 ORM 會自動批次查詢,避免每筆資料都產生額外的查詢。

總結

最後總結一下,如果我們在關聯資料查詢中,採用以下模式:

  1. 先查一次主資料
  2. 再針對主資料的每一筆記錄,逐筆查詢其關聯資料
  3. 最終形成 N+1 次查詢,而這些查詢其實可以整併為少數幾次

就會產生典型的 N+1 問題。

這類問題在資料量變大時,就會照成效能上的問題。

解法的核心,是避免在迴圈中查資料,改用 JOIN、IN 查詢或 ORM 的 eager loading,做查詢批次化,直接把資料一次帶回。

你可能會感興趣的文章 👇

N + 1 問題是什麼?後端查詢的必要知識 | ThisWeb