N+1問題

はじめに

Ruby on RailsのN+1問題に関して調べたので、その忘備録

N+1問題とは

テーブルに対してデータ検索を行うとき、不必要に大量のSQL(N+1件のSELECT文)を発行してしまい、パフォーマンスが低下してしまう問題のこと。

1:多などモデルの関連付けをしている場合に起きやすい

例) UserとArticleの二つのモデルがあるとする。

app/models/user.rb

class User > ActiveRecord::Base
    has_many :articles
end

app/models/article.rb

class Article > ActiveRecord::Base
    belongs_to :user
end

このようにアソシエーションを記述した状態で、以下のようなアクションを設定する。

app/controllers/users_controller.rb

class ArticlesController < ApplicationController
     def index
        User.all.each do |u|
            article_name = u.articles.name
        end
     end
end

この状態でこのアクションを呼び出すと、

SELECT 'users'.* FROM 'users' # User.all の実行

SELECT 'articles'.'name' FROM 'articles' WHERE 'articles'.'id' = 1 LIMIT 1 # user.articles.name を実行する際に走る
SELECT 'articles'.'name' FROM 'articles' WHERE 'articles'.'id' = 2 LIMIT 1
...

と、Articleの数だけ

SELECT 'articles'.name FROM 'articles' WHERE 'articles'.'id' = n LIMIT 1

が走ってしまう。

なぜ生じるのか?

上記の例では、まずUserのデータを全部取得したあと、article_name = u.articles.name毎にArticleのデータベースにアクセスしてデータをとってくるように記述している。

そのせいで、「Userテーブルに対して一回+Articleテーブルに対してUser数ぶん」の回数のSQLが発行されることになる。

解決方法

includesメソッドを使う。

app/controllers/users_controller.rb

class ArticlesController < ApplicationController
     def index
        User.includes(:article).all.each do |u|
            article_name = u.articles.name
        end
     end
end

このように、「モデルA.includes(:モデルB)」とすると、Aに基づくBをまとめて取得できるようになる。

上記の例だと、 「Userテーブルに対して一回+Articleテーブルに対してUser数ぶんをまとめて一回」の2回のSQLで済むようになる。