N+1 query 是什麼?
更新日期: 2024 年 12 月 12 日
N+1 query 是一種常見的性能問題,通常出現在資料庫查詢的情境中,特別是在使用 ORM(Object-Relational Mapping)框架時。
例如,Django、Rails 或 SQLAlchemy 中,當程式執行過多的獨立查詢來取得相關資料時,就會發生這種問題。
為什麼叫 N+1 query?
假設有一個父項目(如一組用戶)和子項目(如用戶的文章)。
如果你想要查詢每個用戶及其文章,程式可能會:
- 執行一次查詢來取得所有用戶 (這是第 1 個查詢)。
- 對於每個用戶,執行額外的查詢來取得他們的文章(對於 N 個用戶,就會執行 N 個查詢)。
這樣總共執行了 1+N1 + N 次查詢,稱為 N+1 query 問題。
問題的影響
N+1 query 問題可能會嚴重影響效能,特別是在資料量大的情況下。例如:
- 如果有 1000 個用戶,那麼程式會執行 1001 次查詢。
- 每次查詢都需要耗費時間來連接資料庫、執行 SQL,並傳輸資料回應。
例子
假設有兩個表格:users
和 posts
。
- 資料結構:
-- users 表
id | name
----+------
1 | Alice
2 | Bob
-- posts 表
id | user_id | title
----+---------+---------
1 | 1 | Post 1
2 | 1 | Post 2
3 | 2 | Post 3
- 程式碼:不良範例(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 次查詢。
- 解決方式:使用關聯查詢:
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 的方法
- 使用關聯式查詢(Eager Loading):
- Django:
select_related()
或prefetch_related()
- Rails:
includes
或eager_load
- SQLAlchemy:
joinedload
或subqueryload
- Django:
- 批量查詢:
- 把多次的小查詢合併成一次大查詢。
- 使用 JOIN 或 IN 條件。
- 檢查查詢數量:
- 使用工具如 Django Debug Toolbar、Rails Bullet gem 來檢測查詢數量。
總結
N+1 query 是指由於不當的查詢設計,導致程式執行過多查詢的問題。
解決方式通常是透過關聯查詢和批量查詢來減少查詢次數。避免 N+1 query 不僅可以提升效能,還能讓程式碼更具可擴展性。