N+1 Query Problem

Larry Lai Lv1

前言

最近剛從 NET 6 跳來 Rails,免不了要踩到一些新手必踩的坑。像是這個 N+1 查詢問題就是很常見的坑…

尤其對我這個用 EF 習慣的人,跳到 ActiveRecord 真的是各種不適應。不過不是不好用的不適應,而是那種方便到很沒有安全感的那種便利XD

問題描述

假設你有一個 Customer 和他們的 Order 資料表。當你查詢 Customer 資料時,如果每個 Customer 都有多個 Order,而你同時查詢這些 Order,就會發生 N+1 的查詢問題。

所以當你執行的一個查詢,實際上會有下面兩個動作:

  1. 先執行一個查詢來取得所有 Customer。
  2. 然後對每個Customer 執行一個查詢來取得該 Customer 的 Order。

如果有 N 個 Customer,你將會執行 1 + N 個查詢,這會導致效能問題。

簡單來說,N+1 查詢問題就是當你需要多次查詢資料庫才能取得完整資料時,會造成大量不必要的查詢,影響效能。

解決方法

includes

1
Customer.includes(:orders).all

把需要的資料一次撈出來,轉換成 SQL 語法的話就是使用了 IN (‘1’,’2’,’3’) 這樣的方式來一次性預先載入。避免重複向資料庫存取。

eager_load

1
Customer.eager_load(:orders).all
  1. eager_load 強制使用 LEFT OUTER JOIN 來載入關聯資料。在一個查詢中同時載入主要資料和關聯資料。
  2. 適合在需要對關聯資料進行條件篩選或排序時使用。

preload

1
Customer.preload(:orders).all

preload 強制使用多次查詢來載入關聯資料。它會先查詢主要資料,再查詢關聯資料,這樣可以避免 JOIN 查詢的開銷。
適合在不需要對關聯資料進行條件篩選時使用。

上述提到的多次查詢 是會轉換成下面這樣的 SQL 語法:

1
2
3
4
-- 查詢顧客
SELECT * FROM customers;
-- 查詢所有相關顧客的訂單
SELECT * FROM orders WHERE orders.customer_id IN (1, 2, 3, ...);

總結

一般來說都是直接選擇使用includes,因為 includes 會選擇最優的查詢策略。它會根據上下文決定是使用單獨查詢還是 JOIN 查詢。

當只讀取資料而沒有條件篩選時,通常會進行兩次查詢(preload)。

當涉及條件篩選並需要 JOIN 查詢時,它會自動使用 LEFT OUTER JOIN(eager_load)。

  • 標題: N+1 Query Problem
  • 作者: Larry Lai
  • 撰寫于 : 2024-07-17 18:00:00
  • 更新于 : 2024-07-18 09:38:06
  • 連結: https://redefine.ohevan.com/2024/07/17/n-plus-one-problem/
  • 版權宣告: 本作品采用 CC BY-NC-SA 4.0 进行许可。
 留言