N+1 query 是什麼?

更新日期: 2024 年 12 月 12 日

N+1 query 是一種常見的性能問題,通常出現在資料庫查詢的情境中,特別是在使用 ORM(Object-Relational Mapping)框架時。

例如,Django、Rails 或 SQLAlchemy 中,當程式執行過多的獨立查詢來取得相關資料時,就會發生這種問題。

為什麼叫 N+1 query

假設有一個父項目(如一組用戶)和子項目(如用戶的文章)。

如果你想要查詢每個用戶及其文章,程式可能會:

  1. 執行一次查詢來取得所有用戶 (這是第 1 個查詢)。
  2. 對於每個用戶,執行額外的查詢來取得他們的文章(對於 N 個用戶,就會執行 N 個查詢)。

這樣總共執行了 1+N1 + N 次查詢,稱為 N+1 query 問題

問題的影響

N+1 query 問題可能會嚴重影響效能,特別是在資料量大的情況下。例如:

  • 如果有 1000 個用戶,那麼程式會執行 1001 次查詢。
  • 每次查詢都需要耗費時間來連接資料庫、執行 SQL,並傳輸資料回應。

例子

假設有兩個表格:usersposts

  1. 資料結構
-- users 
id | name
----+------
 1  | Alice
 2  | Bob

-- posts 
id | user_id | title
----+---------+---------
 1  |    1    | Post 1
 2  |    1    | Post 2
 3  |    2    | Post 3
  1. 程式碼:不良範例(N+1 query 問題):
users = User.objects.all()  # 第 1 個查詢
for user in users:
    posts = Post.objects.filter(user_id=user.id)  # 每個 user 再執行 1 次查詢
  • 如果有 2 個用戶,程式執行的 SQL:
SELECT * FROM users;
SELECT * FROM posts WHERE user_id = 1;
SELECT * FROM posts WHERE user_id = 2;
  • 總共 1(用戶查詢)+ 2(每個用戶的文章查詢)= 3 次查詢。
  1. 解決方式:使用關聯查詢
users = User.objects.prefetch_related('posts')  # 一次載入所有相關資料
for user in users:
    posts = user.posts.all()
  • 執行的 SQL:
SELECT * FROM users;
SELECT * FROM posts WHERE user_id IN (1, 2);
  • 現在只需要 2 次查詢,而不是 N+1 次。

解決 N+1 query 的方法

  1. 使用關聯式查詢(Eager Loading):
    • Django: select_related()prefetch_related()
    • Rails: includeseager_load
    • SQLAlchemy: joinedloadsubqueryload
  2. 批量查詢
    • 把多次的小查詢合併成一次大查詢。
    • 使用 JOIN 或 IN 條件。
  3. 檢查查詢數量
    • 使用工具如 Django Debug Toolbar、Rails Bullet gem 來檢測查詢數量。

總結

N+1 query 是指由於不當的查詢設計,導致程式執行過多查詢的問題。

解決方式通常是透過關聯查詢和批量查詢來減少查詢次數。避免 N+1 query 不僅可以提升效能,還能讓程式碼更具可擴展性。

Similar Posts