在 Ruby on Rails 中,透過 ORM (Object Relational Mapping) 使我們可以輕易地對不同表進行操作,方便之餘,一不小心就可能會寫出 N+1
後續的文章會以此 repo 作為範例
N+1 指的是 SQL 撈資料時,明明可以一次撈完 (ex: 3 筆資料),卻使用逐筆撈資料的方式處理 (每次只撈 1 筆資料)
白話文: 使用者要在同間商店買 3 件商品,可一次採買完成,卻分成 3 次購買 (除非是想收集發票
分 3 次購買便是 N ,而 +1 則是使用者決定要去買的那次本身
導致總共撈了 3+1 次,嚴重影響效能
直接看例子,會比較好懂,想看 N+1 本人的話,可參考此 commit
# app/models/user.rb
class User < ApplicationRecord
has_many :orders
end
# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
end
# app/controllers/users_controller.rb
def index
@users = User.all
end
# app/views/users/index.html.erb:24
<td><%= user.orders.sum(&:total_price) %></td>o
上述可以正常運作,但也造成了 N+1 的問題發生
# Order 被撈了 3 次
User Load (0.3ms) SELECT "users".* FROM "users"
Order Load (0.2ms) SELECT "orders".* FROM "orders" WHERE "orders"."user_id" = $1 [["user_id", 1]]
Order Load (0.2ms) SELECT "orders".* FROM "orders" WHERE "orders"."user_id" = $1 [["user_id", 2]]
Order Load (0.3ms) SELECT "orders".* FROM "orders" WHERE "orders"."user_id" = $1 [["user_id", 3]]
最簡單方法,使用 includes
,可參考此 commit
雖然每次會撈回整筆 orders 資訊 (假如我只需要 order.total_price
的資訊,其餘欄位皆不用的話),會撈比較多東西回來,資料量如果不多的話,includes
是一種解法,資料量多時,會有不同解法 (未來有機會再另寫文章探討
# app/controllers/users_controller.rb
def index
@users = User.includes(:orders)
end
# Order 變成一次查詢完畢
User Load (0.2ms) SELECT "users".* FROM "users"
Order Load (0.4ms) SELECT "orders".* FROM "orders" WHERE "orders"."user_id" IN ($1, $2, $3) [["user_id", 1], ["user_id", 2], ["user_id", 3]]
includes
解 N+1可使用 Bullet 這個 Gem,並設定提醒視窗,方便知道是否有 N+1 的問題發生
在 Gemfile 加入以下,可參考此 commit
# Gemfile
gem 'bullet', group: 'development'
# 記得要 bundle
接著在 development.rb
檔案增加參數設定,可參考此 commit
備註: 更多設定可至官方網站查詢
# config/environments/development.rb
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.add_footer = true
# 接著重啟 rails server
Ruby on Rails - 用 Include 和 Join 避免 N+1 Query
造成網站效能拖慢有許多可能,N+1 只是其中一種
本篇是淺談其中的 1 種解法,寫這篇時,發現類似文章不下 5 篇以上,若想深入了解,歡迎善用 Google
鐵人賽文章連結:https://ithelp.ithome.com.tw/articles/10244142
medium 文章連結:https://link.medium.com/w4c3ApZw49
本文同步發布於 小菜的 Blog https://riverye.com/
備註:之後文章修改更新,以個人部落格為主