你必須很努力

Day21 - Ruby on Rails 中常見的 N+1 與解法

2020/09/26
字數統計: 758閱讀時間: 3 min

前言

在 Ruby on Rails 中,透過 ORM (Object Relational Mapping) 使我們可以輕易地對不同表進行操作,方便之餘,一不小心就可能會寫出 N+1

  1. 到底什麼是 N+1 ?
  2. 該如何解決 ?

後續的文章會以此 repo 作為範例


介紹

N+1 指的是 SQL 撈資料時,明明可以一次撈完 (ex: 3 筆資料),卻使用逐筆撈資料的方式處理 (每次只撈 1 筆資料)

白話文: 使用者要在同間商店買 3 件商品,可一次採買完成,卻分成 3 次購買 (除非是想收集發票

分 3 次購買便是 N ,而 +1 則是使用者決定要去買的那次本身

導致總共撈了 3+1 次,嚴重影響效能

直接看例子,會比較好懂,想看 N+1 本人的話,可參考此 commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 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 的問題發生

1
2
3
4
5
6
# 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]]

N+1 本人

如何解決 N+1

最簡單方法,使用 includes,可參考此 commit

雖然每次會撈回整筆 orders 資訊 (假如我只需要 order.total_price 的資訊,其餘欄位皆不用的話),會撈比較多東西回來,資料量如果不多的話,includes 是一種解法,資料量多時,會有不同解法 (未來有機會再另寫文章探討

1
2
3
4
# app/controllers/users_controller.rb
def index
@users = User.includes(:orders)
end

1
2
3
4
# 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

如何知道哪些地方有 N+1

可使用 Bullet 這個 Gem,並設定提醒視窗,方便知道是否有 N+1 的問題發生

安裝方式

在 Gemfile 加入以下,可參考此 commit

1
2
3
4
5
# Gemfile

gem 'bullet', group: 'development'

# 記得要 bundle

接著在 development.rb 檔案增加參數設定,可參考此 commit

備註: 更多設定可至官方網站查詢

1
2
3
4
5
6
7
8
# config/environments/development.rb

Bullet.enable = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.add_footer = true

# 接著重啟 rails server

若有 N+1 會有提醒視窗

參考資料

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/

備註:之後文章修改更新,以個人部落格為主

原文連結:https://riverye.com/2020/09/26/Day21-Ruby-on-Rails-中常見的-N-1-與解法/

發表日期:2020-09-26

更新日期:2022-12-21

CATALOG
  1. 1. 前言
  2. 2. 介紹
    1. 2.0.1. N+1 本人
  • 3. 如何解決 N+1
    1. 3.0.1. 用 includes 解 N+1
  • 4. 如何知道哪些地方有 N+1
    1. 4.1. 安裝方式
      1. 4.1.1. 若有 N+1 會有提醒視窗
  • 5. 參考資料
  • 6. 小結