RailsでSQLのN+1問題を回避する
みなさんこんにちは、新人の意識高丸です。
先日も初心者Ruby勉強会gaienrb#26に参加しました。
今回はN+1問題について考えてみました。
まずN+1問題とは、例えば一覧系の画面などの一覧データを取得する際に、各行ごとにSQLを発行してしまう問題です。
RubyOnRailsで開発している場合でも、モデルのアソシエーションを単純に設定して利用するだけではこの問題に陥る場合があります。
例えば以下の様なテーブル関連があったとします。
## テーブル構造
プロジェクトテーブル
—
projects
name
タスクテーブル
—
tasks
project_id
title
deadline
done
メモテーブル
—
memos
task_id
memo_text
## アソシエーション設定
“`app/models/project.rb
class Project < ActiveRecord::Base
has_many :tasks
end
```
```app/models/task.rb
class Task < ActiveRecord::Base
belongs_to :project
has_many :memos
end
```
```app/models/memo.rb
class Memo < ActiveRecord::Base
belongs_to :task
end
```
プロジェクトは複数のタスクを持つことができ、タスクは複数のメモを持つことができます。
この関連設定で一つのprojectのtasksとそのメモをforeachで以下のように出力した場合、各行ごとにSQLが発行され、
以下のようにprojectの取得、taskの取得に加えて、各タスクでのメモの取得のSQLが行数分実行されます。
```Projectの一覧と紐づくtask,memoを取得するプログラム
Project.all.each do |project|
p project.name
project.tasks.each do |task|
p task.title
task.memos.each do |memo|
p memo.memo_text
end
end
end
```
```発行されるn+1のSQL
Project Load (0.3ms) SELECT "projects".* FROM "projects"
Task Load (0.2ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = ?
[["project_id", 1]]
Memo Load (0.2ms) SELECT "memos".* FROM "memos" WHERE "memos"."task_id" = ?
[["task_id", 1]]
Memo Load (0.1ms) SELECT "memos".* FROM "memos" WHERE "memos"."task_id" = ?
[["task_id", 2]]
Memo Load (0.2ms) SELECT "memos".* FROM "memos" WHERE "memos"."task_id" = ?
[["task_id", 3]]
Task Load (0.3ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = ?
[["project_id", 2]]
Memo Load (0.2ms) SELECT "memos".* FROM "memos" WHERE "memos"."task_id" = ?
[["task_id", 4]]
Memo Load (0.2ms) SELECT "memos".* FROM "memos" WHERE "memos"."task_id" = ?
[["task_id", 5]]
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = ?
[["project_id", 3]]
```
これだけのSQLが実行されているとなると、パフォーマンスに影響するということはすぐわかります。
SQLを自分で書いて回避するという手もありますが、せっかくActiveRecordを使っているのに生のSQLを書きたくないので、
N+1を回避する方法を探したところ、検索時ににincludesを指定して回避することができました。
```
@projects = Project.all.includes(:tasks => :memos)
“`
検索時以外にも、モデルのアソシエーションに以下のように設定することでも回避出来ます
。
“`
class Project < ActiveRecord::Base
has_many :tasks ,-> {includes :memos}
end
“`
上記のようにアソシエーションに設定した場合はfindで検索時にincludeを指定する必要はありません。
このようにincludesを適切に設定すると、実行されるSQLは以下の通り少なくなります。
“`
Project Load (0.2ms) SELECT “projects”.* FROM “projects”
Task Load (0.4ms) SELECT “tasks”.* FROM “tasks” WHERE “tasks”.”project_id” IN
(1, 2, 3)
Memo Load (0.4ms) SELECT “memos”.* FROM “memos” WHERE “memos”.”task_id” IN (1,
2, 3, 4, 5)
“`
ちなみに、Bulletというgemを導入すると、N+1問題のある画面で警告を出してくれるようになります。
Bulletを導入するにはまずGemfileに以下の記述をしてbundle installを実行します
“`Gemfile
group :development do
gem “bullet”
end
“`
次に、development.rbに以下のように記述します
“`config/environments/development.rb
AppName::Application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
end
end
“`
そうすると画面にアクセスした際にN+1問題を検知すると、以下のようにAlertで警告を表示するようになります
“`警告メッセージ
user: takamaru
N+1 Query detected
Project => [:tasks]
Add to your finder: :include => [:tasks]
N+1 Query method call stack
/Users/takamaru/Develop/gaien.rb/rails_projects/task_manager/example/app/views/proj
ects/index.html.erb:19:in `block in
_app_views_projects_index_html_erb__1152048756454081808_70323602535080′
/Users/takamaru/Develop/gaien.rb/rails_projects/task_manager/example/app/views/proj
ects/index.html.erb:15:in
`_app_views_projects_index_html_erb__1152048756454081808_70323602535080′
“`
development.rbで出力方法を指定できます。
例えばBullet.alert = falseと指定すると、Alertでは警告が出力されなくなります。
Rubyについて日々勉強している新人エンジニアです。初心者向けRuby勉強会のレポートなどを投稿していきます。
TAG
- Android
- AWS
- Bitrise
- CodePipeline
- Firebase
- HTML
- iOS
- IoT
- JavaScript
- KPI
- Linux
- Mac
- Memcached
- MGRe
- MGReのゆるガチエンジニアブログ
- MySQL
- PHP
- PICK UP
- PR
- Python
- Ruby
- Ruby on Rails
- SEO
- Swift
- TIPS
- UI/UX
- VirtualBox
- Wantedly
- Windows
- アクセス解析
- イベントレポート
- エンジニアブログ
- ガジェット
- カスタマーサクセス
- サーバ技術
- サービス
- セキュリティ
- セミナー・展示会
- テクノロジー
- デザイン
- プレスリリース
- マーケティング施策
- マネジメント
- ラボ
- リーンスタートアップ
- 企画
- 会社紹介
- 会社紹介資料
- 勉強会
- 実績紹介
- 拡張性
- 採用
- 日常
- 書籍紹介
- 歓迎会
- 社内イベント
- 社員インタビュー
- 社長ブログ
- 視察
- 開発環境