RailsでSQLのN+1問題を回避する

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では警告が出力されなくなります。

TAG

  • このエントリーをはてなブックマークに追加
意識 高丸
エンジニア 意識 高丸 takamaru

Rubyについて日々勉強している新人エンジニアです。初心者向けRuby勉強会のレポートなどを投稿していきます。