Rails 中 includes 跟 joins 的差別

Rails 中 includesjoins 的差別

前言

最近幫忙在調校網站 n+1 的問題,所以想來寫個筆記,分成兩篇。

  1. Rails 中 includesjoins 的差別
  2. Rails 的 lengthcountsize 差別?

何謂 N+1 問題

在 Rails 中建立 model 的資料關聯十分的容易,但也是因為建立方便,所以很容易就會寫出效能差的程式碼,每一次進出 Database 都會消耗很大的效能,到底何謂 N+1 問題呢?
舉個例子:

1
2
3
4
5
# User
has_many :orders

#Order
belongs_to :user

當我們想在畫面上印出所有 ordersuser.email,此時所造成的 query 就會長這樣:

上面的寫法就會先將所有 orders,找出來,在針對每一筆的 order.user_id 去查找users的表,共有 9 筆,所以找 9 次。

若有 n 筆就找 n 次,再加上一開始找出所有 ordersquery ,就變成了 n+1

若資料量稍微龐大一點,效能就會變差。

請善用 includes

而 Rails 很貼心的有個語法可以讓我們避免這樣的問題,includes
若將 orders = Order.all 改成 orders = Order.all.includes(:user)

那在最一開始撈出所有 order 時,會順便一起把相對應的 user 找出來,此時繼續輸入:

1
2
3
orders.each do |order|
order.user.email
end

這時就不會進到 Database 中查詢。
從 n+1 變成 1+1 👍

includes 也支援巢狀關聯 (使用 hash)

如果除了 user ,還想一起找出跟 user 有關聯的 model 可以使用 hash 的寫法

1
2
3
4
5
# 以 profile 為例
# User has_many profiles

orders = Order.all.includes(user: [ :profile ])
# 可以順便把 profile 撈出來

還有一個語法叫 joins

除了 includes 之外,還有一個很像的語法叫做 joins,不過差別為 joins 是過濾 model 之間的關聯,像上方的寫法,在印出 user.email 的時候還是會產生查詢!

1
2
orders = Order.all.joins(:user)
# 回傳具有 user_id 的 order

has_many 又會跟 belongs_to 的情境有不一樣的回傳結果

1
2
# User has_many :orders
user = User.joins(:orders)
  • 查詢所有具有 user_idorder,並回傳該 orderuser ,若有很多 order 所屬同一個 user,則回傳值會包含大量同一名 user 的資料,可以用 uniq 處理。

來個總結!

簡單來說,若是要處理大量資料,會比較建議使用 includes,以避免 n+1 的問題,而 joins 的話,目前推論是需要對資料做關聯 model 的條件篩選,才會比較建議使用。


參考文章

Rails使用 include 和 join 避免 N+1 query

Ruby on Rails 實戰聖經 - 網站效能

官方 API 文件

stackoverflow