こんにちは、Railsエンジニアにょけんです。
allメソッドなどを使うと度々起こる「N+1問題」について、概要と解決策をまとめました。
「N+1問題」とは、データベースへのアクセスが非効率な状態のこと
超カンタンに言っちゃうと、「N+1問題」はRailsさんがデータベースを有効活用できていない状態です。
具体的には、1回のアクセスで済むところを何回もアクセスしてしまっている感じ。
「コンビニでパン・おにぎり・飲み物買ってきて」と頼んだら、まずパンを買って帰ってきて、次におにぎりを買ってまた帰ってきて、次に飲み物を買ってくるみたいな。

って思いません?
まさにこれが「N+1問題」発生中の状態。
次から具体的に書いていきます。
「N+1問題」は、1:多などモデルの関連付けをしている場合に起きやすい
「User:Post=1:多」の状態を想定します。
class User < ActiveRecord::Base has_many :posts end
class Post < ActiveRecord::Base belongs_to :user end
データベースはこんな感じ。
User_Table |
||
id | name | age |
1 | Ken | 30 |
2 | Sara | 24 |
3 | Mike | 17 |
Post_Table |
||
id | user_id | content |
1 | 1 | Hello, I’m Ken. |
2 | 2 | Hi, I’m Sara. |
3 | 1 | I’m sleepy. |
4 | 3 | Eating now. |
5 | 1 | Good morning. |
各Postに、Userが紐付いています。
このとき、各Userのpost.contentを表示したかったら、以下のように書きますよね?
@users = User.all @users.each do |user| puts user.post.content end
これを実行すると、
①まず「Userモデル」を全て取得(User,all)
User_Table |
||
id | name | age |
1 | Ken | 30 |
2 | Sara | 24 |
3 | Mike | 17 |
↑こいつを読み込む
②最初の「puts user.post.content」で、Postのデータベースにアクセスしてuser_id=1のデータを取ってくる(以下緑色の部分)
Post_Table |
||
id | user_id | content |
1 | 1 | Hello, I’m Ken. |
2 | 2 | Hi, I’m Sara. |
3 | 1 | I’m sleepy. |
4 | 3 | Eating now. |
5 | 1 | Good morning. |
③次の「puts user.post.content」で、またPostのデータベースにアクセスしてuser_id=2のデータを取ってくる(以下緑色の部分)
Post_Table |
||
id | user_id | content |
1 | 1 | Hello, I’m Ken. |
2 | 2 | Hi, I’m Sara. |
3 | 1 | I’m sleepy. |
4 | 3 | Eating now. |
5 | 1 | Good morning. |
④最後に「puts user.post.content」で、Postのデータベースにアクセスしてuser_id=3のデータを取ってくる(以下緑色の部分)
Post_Table |
||
id | user_id | content |
1 | 1 | Hello, I’m Ken. |
2 | 2 | Hi, I’m Sara. |
3 | 1 | I’m sleepy. |
4 | 3 | Eating now. |
5 | 1 | Good morning. |
このように、「Userテーブルに対して1回+Postテーブルに対してUser数の3回=計4回」のアクセスが起きます。

感覚的には、「1+N問題」って言った方が分かりやすいかも。
これって、「user_id=1〜3をまとめて取得できれば、②〜④を1回で済ませられる」はずですよね?
そのやり方を次で解説します。
「N+1問題」は、includesメソッドで解決!
「N+1問題」を解決するには、Railsのincludesメソッドを使いましょう。
「include=含む」という意味で、「モデルA.include(:モデルB)」とすると、Aに基づくBを取得できます。
今回は、Userに紐付くPostを取得したいので以下のように書きます。
@users = User.includes(:post).all @users.each do |user| puts user.post.content end
これを実行すると、
①まず「Userモデル」を全て取得(ここは同じ)
User_Table |
||
id | name | age |
1 | Ken | 30 |
2 | Sara | 24 |
3 | Mike | 17 |
↑こいつを読み込む
②その後、Postのデータベースにアクセスして「user_id=1〜3のデータ」を取ってくる(以下緑色の部分)
Post_Table |
||
id | user_id | content |
1 | 1 | Hello, I’m Ken. |
2 | 2 | Hi, I’m Sara. |
3 | 1 | I’m sleepy. |
4 | 3 | Eating now. |
5 | 1 | Good morning. |
こんな感じで、「N+1」のN部分を1回にまとめられます。
「N+1問題」発生中と、includes使った時の違いをSQLで表すと…
ついでにSQLで表すとどんな感じに違うのか書いておきます。
「N+1問題」発生中
@users = User.all @users.each do |user| puts user.post.content end
SQLで表すと…
SELECT 'users'.* FROM 'users' SELECT 'posts'.* FROM 'posts' WHERE 'posts'.'user_id' = 1 SELECT 'posts'.* FROM 'posts' WHERE 'posts'.'user_id' = 2 SELECT 'posts'.* FROM 'posts' WHERE 'posts'.'user_id' = 3
「includes」使用
@users = User.includes(:post).all @users.each do |user| puts user.post.content end
SQLで表すと…
SELECT 'users'.* FROM 'users' SELECT 'posts'.* FROM 'posts' WHERE 'posts'.'user_id' IN (1,2,3)
allでは「=」で1つ1つ取得していたのを、「IN」というメソッドでまとめて取得しています。
【応用】いろんな関連付けでの「includes」
シンプルなincludesはカンタンですが、関連付けが複雑化するに連れてめんどくさくなります。
「1 対 多」の場合
さっき説明した事例です。
「User:Post=1:多」とすると、以下でOK
@users = User.includes(:post).all
ちなみに、Postに基づくUserを取得したければ、関係性を入れ替えるだけ。
@posts = Post.includes(:user).all
SQLで表すと…
SELECT 'posts'.* FROM 'posts' SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (1,2,3)
ちょっとした変化としては、idが普通にuser.idになります。(user.post_idっていうのはないので)
「多 対 1 対 多 」の場合
UserがPostとLike(お気に入り)の2つをhas_manyな場合に、「あるpostのuserのlikesを表示したい(=post投稿者のお気に入りを表示したい)」とします。
今まで通り
@posts = Post.include(:user).all @posts.each do |post| puts post.user.likes end
とすると、PostとUser間の「N+1問題」は解決できますが、UserとLike間で「N+1問題」が発生します。
解決するには、
@posts = Post.includes(user: :like).all
とすればOK
汎用的に書くと、「多A : 1: 多B」の場合は、「多A.includes(1: :多B)」としましょう。
ちなみにSQLで表すと…
SELECT 'posts'.* FROM 'posts' SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (1,2,3) SELECT 'likes'.* FROM 'likes' WHERE 'likes'.'user_id' IN (1,2,3)
という処理が走ります。
「1 対 多×複数 」の場合
1つ上の例では、post.user.likesで「postに紐付くuserを取得→そのuserのlikesを取得」といったように、postとlikesを1文の中に使っていましたよね?
そうではなく、単純に「user.posts」「user.likes」の2つを表示したいだけの場合は、以下のコードで大丈夫です。
@users = User.include(:post, :like).all @users.each do |user| puts user.posts puts user.likes end
SQLでは…
SELECT 'users'.* FROM 'users' SELECT 'posts'.* FROM 'posts' WHERE 'posts'.'user_id' IN (1,2,3) SELECT 'likes'.* FROM 'likes' WHERE 'likes'.'user_id' IN (1,2,3)
という処理が走ります。
コメント