N+1問題とは?解決方法について

rubyのイメージ
Ruby on Railsの学習を進めて行くと、N+1問題というSQLの問題に直面します。この問題はRuby on Railsに限った話ではないのですが、よく取り上げられるので詳しく見ていきたいと思います。

N+1問題とは何か

N+1問題とは無駄に(必要よりも多く)SQLを実行させてしまい、パフォーマンスを低下させてしまう問題のことを言います。
例えば下記のコードを見てみましょう。
UserとMessageのモデルがあるとして、

# app/modules/user.rb
class User < ActiveRecord::Base
  has_many :messages
end
# app/models/message.rb
class Article < ActiveRecord::Base
  belongs_to :user
end

上の例では、Userが複数のMessageを持っています。
続けてViewでMessageのcontent(文章)を表示するとしましょう。

# app/controllers/users_controller.rb
class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end
end
# app/views/articles/index.html.haml
- @messages.each do |message|
  = message.content
  = message.user.name

このコードを実行するとSQLのログは下記になります。

SELECT 'messages'.* FROM 'messages' # Message.all の実行
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 1 LIMIT 1 # message.user.name を実行する際に走る
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1

このようにmessageの数だけ

SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = n LIMIT 1

が実行されてしまいます。
これがパフォーマンスの低下につながり、N+1問題と言われています。

解決方法

1対Nの場合

1対Nはここまで説明してきたUserとMessageの関係です。
この場合コントローラで@messagesを取得する際に、

@messages = Message.all.includes(:user)
SELECT 'message'.* FROM 'messages'
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (1, 2)

の2つの SQL が発行されるだけになり、N+1 問題が解決します。

まとめ

N+1問題の解決方法は上記の通りです。N対Nの記述方法については別の記事に書く予定です。
N+1問題は確かにパフォーマンスを下げますが、コードの可読性も下げることになります。場合によってはそこまで気にする必要もないパターンもあるようなので、これから学んでいければと思っています。
参考記事:Rails で includes して N+1 問題対策 - Qiita