ORM — N+1 Problem

Hung
4 min readMay 9, 2020

--

ORM 是程式與資料庫溝通不能缺少的工具,ORM 妥善的隱藏了背後資料庫的查詢,使得查詢條件模組化,大幅提昇了重用與可讀性,在簡單的情境下開發,甚至可以不需要會各種 Query 語法,但隨著商業邏輯逐漸複雜,看不到的細節可能衍生意想不到的問題,最常見就是 N+1 的問題。

N+1 Problem
當查詢的條件,像是 where 或是一對多的關聯這種查詢時,返回的條件可能是多個結果,可以預期接收的到型態是 Array 或是 Collect,當我們再對得到的 Collect 做遍歷,取得關連資料時,ORM 會對應每次的關連查詢下一次條件,也就是說當使用者有十筆訂單,查出使用者的十筆訂單後,想接著取得訂單商品的資訊,若我們直接遍歷查詢訂單商品,就會產生十筆查詢訂單商品的 Query。

範例 ( pseudocode)

user_order_items = []user = User.find(9527)
# SELECT * FROM `users` where `id` = 9527
user.orders.each do |order|
user_order_items << order.order_items
end
# SELECT * FROM `orders` where `user_id` = 9527
# payment_ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# SELECT * FROM `order_items` where `order_id` = 1
# SELECT * FROM `order_items` where `order_id` = 2
# SELECT * FROM `order_items` where `order_id` = 3
# SELECT * FROM `order_items` where `order_id` = 4
# SELECT * FROM `order_items` where `order_id` = 5
# SELECT * FROM `order_items` where `order_id` = 6
# SELECT * FROM `order_items` where `order_id` = 7
# SELECT * FROM `order_items` where `order_id` = 8
# SELECT * FROM `order_items` where `order_id` = 9
# SELECT * FROM `order_items` where `order_id` = 10
or user_order_items = user.orders.collect(&:order_items).flatten

在這樣的情況下,可能會有人試圖在 order_items 加上 user_id 再建立與 User 的關聯,這樣做會失去了資料表的階層關係,在資料變動的時候,都必須記得更新相關的資料表,以這個例子來說若刪除 User.orders 而沒有同時刪除 User.order_items,就會出現 user 有 order_items 卻沒有 order 的情況,會導致資料的完整性遭到破壞,後續會相當難維護。

有些 ORM 有提供跨表關連的方式,自動幫你處理 join 之間的關係,避免上述問題,但在使用上還是需要斟酌,過多的跨表查詢會導致 Model 與 table 的關係變得不明確。

範例

# User Model
has_many :order_items, through: :order
> user.order_items# query
SELECT
`order_items`.*
FROM `order_items`
INNER JOIN `orders` ON `order_items`.`order_id` = `orders`.`id`
WHERE `orders`.`user_id` = 9527

而解決 N+1 的常見方式是預載需要用到的 Model,預載的實現有好幾種方法,但概念上都是預先定義好關連的查詢方式,避免 ORM 無腦產生多個重複 Qeury。

範例

user = User.preload(orders: :order_items).find(9527)order_item_ids = user.orders.collect(&:order_items).flatten.map(&:id)

延伸:Rails ActiveRecord — preload、eager_load、includes

Unlisted

--

--