はじめに
気を緩めてると気づいたら N+1 問題って起きてたりする。
N+1 問題とは何か
簡単に言うと必要以上に SQL が走るせいでパフォーマンスが低下する問題です。
例えば
User と Article の2つのモデルがある状態で、
class User < ActiveRecord::Base
has_many :articles
end
class Article < ActiveRecord::Base
belongs_to :user
end
上記のように User が複数の Article を持っているとする。
そして下記のコードで Article の title を表示する。
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
end
- @articles.each do |article|
= article.title
= article.user.name
このコードだと以下のようなログを見ると SQL が発行される。
SELECT 'articles'.* FROM 'articles' # Article.all の実行
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 1 LIMIT 1 # article.user.name を実行する際に走る
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1
このように Article の数だけ
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = n LIMIT 1
この SQL が発行されることになり、これが N+1 問題。
やりたいこと
N+1 問題を解決したい。
今回は includes
を使って多段ネストの N+1 問題も解決する。
includes
の挙動に関しては今回は省略。
解決方法
1 対 N の場合
つまり先程の例である。この場合コントローラで @articles
を取得する際に
@articles = Article.all.includes(:user)
にすると
SELECT 'article'.* FROM 'articles'
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (1, 2)
の2つの SQL が発行されるだけになり、N+1 問題が解決する。
N 対 1 対 N の場合
先程のモデルに加えて、User が Comment も持っている場合。
class User < ActiveRecord::Base
has_many :articles
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
end
- @articles.each do |article|
= article.title
= article.user.name
- article.user.comments.each do |comment|
= comment.content
上記のコードの時、Article の取得を先程と同じように
@articles = Article.all.includes(:user)
とすると、article.user.comments
の user.comments
部分で N+1 問題が起きてしまう。
この場合は
@articles = Article.all.includes(user: :comments)
とすると N+1 問題が解消される。
N 対 1 対 N 対 1 の場合
先程の例に加えて、さらに comment が 1 つのカテゴリに所属している場合(例がくそみたいに悪いですが気にしないでください。)
- @articles.each do |article|
= article.title
= article.user.name
- article.user.comments.each do |comment|
= comment.content
= comment.category.name
この時は
@articles = Article.all.includes(user: {comments: :category})
で解決する。
またコメントが id が 1 のカテゴリに属しているか調べる場合(あまり使わない)
- article.user.comments.where(category_id: 1).present?
このように view で where
を使ってしまうと、たとえ Article の取得を
@articles = Article.all.includes(user: {comments: :category})
としていても N+1 問題がおきてしまうので、
@articles = Article.all.includes(user: :comments)
として
- article.user.comments.pluck(:category_id).include?(1)
のようにすると N+1 が解決されるはず。
1 対 複数の N の場合
先程までのモデリングで view が
- @users.each do |user|
- user.articles.each do |article|
= article.title
- user.comments.each do |comment|
= comment.content
の場合は、User の取得時に
@users = User.all.includes(:articles, :comments)
とすればよい
まとめ
N+1 問題は確かにパフォーマンスを下げはするが、N+1 問題を解決するために可読性の低いコードになってしまったり、余計に処理が複雑になってしまうことがあるので無理に直さなくてもいい場合もあると思う。
また、今回は includes
を使ったがやり方はケースバイケースで違ったりもするので出くわした状況に応じて対策する必要がある。
途中くそみたいなモデリングのケースを出したが、あれは実際に私がどうしようか割りと悩んだところ。
今回は includes
と include?
を使ったがもっといい方法があれば教えてほしい。