アプリの概要(前提)
・こんな感じで、ユーザーaaaの投稿に対してコメントが6件ついていて、そのコメントと共に、コメントした人のユーザー情報(名前とプロフ画像)が載っている
・/posts/8
にGETリクエストを送るとviews/posts/show.html.slim
が返される
・ユーザーaaaのid
は16
・ユーザーoooのid
は17
・Commentはbodyカラムを、Userはnameカラムとimageカラムを持つ
N+1問題とは
N+1問題とは、データベースからデータを取り出す際に、必要以上にSQLが発行されることで、パフォーマンスが悪くなる(処理速度が遅くなる)問題のこと
具体例
先程のアプリのpostsコントローラー
def show
@post = Post.find(params[:id])
@comments = @post.comments.all.order(created_at: :desc)
end
/ 投稿一覧
/ コメント一覧
<% @comments.each do |comment| %>
<%= comment.user.name %>
<% end %>
<% @comments.each do |comment| %>
のところでpost_id: 8
の全てのComment情報(body)及びコメントしたUserの情報(nameとimage)のSQLが発行される(allメソッド
実行時ではないから注意)
その時に発行されたSQL(rails s
の画面で確認できる)
まず、オレンジ色の線が
post_id: 8
のComment
を全件取得(今回は6件)
次に、赤色の線が
1番目のcommentに紐づいているuserを1人取得(id: 17)
2番目のcommentに紐づいているuserを1人取得(id: 17)
3番目のcommentに紐づいているuserを1人取得(id: 17)
4番目のcommentに紐づいているuserを1人取得(id: 16)
5番目のcommentに紐づいているuserを1人取得(id: 16)
6番目のcommentに紐づいているuserを1人取得(id: 16)
を表している。
このように、post_id: 8
にされたコメントの数(6件)+Comment
を全件取得の1回で、合計7回SQLが発行されてしまう(usersテーブルに対してcommentの数(6回) + commentsテーブルに対して全件取得(1回)) 。
これがN+1問題と呼ばれている。
このままでは、もしpost_id: 8
にされたコメントの数が10000件になれば10001回SQLが発行されることになってしまい、アプリのパフォーマンスの低下につながる。
解決方法
post_id: 8
のComment
を全件取得し、その段階でそれに紐づいているuserを取得すれば、テーブルを参照する回数は2回で済み、コメントが何件になろうともSQLの発行回数は2回で済む。
具体的には、allメソッド
をincludesメソッド
に変えるだけである。
def show
@post = Post.find(params[:id])
@comments = @post.comments.includes.order(created_at: :desc)
end
※ includesメソッド
とは、関連している複数のテーブルからデータを取得してくるときのアクセス回数を大きく減らすことができるメソッド。また、事前に検索やフィルタリング、ソートなどをしたデータを取得することもできるため、アプリケーション側でそれらの処理を行う必要がなくなる
※ 書き方は、モデル名.includes(:関連名)
includesメソッドに渡す引数は、テーブル名ではなくアソシエーションで定義した関連名を指定する(テーブル名でないから注意)
※ オレンジ色の線で、post_id: 8
のComment
を全件取得し、赤色の線でcommentに紐付くuserのデータを取得している。これなら、コメントが10000件になってもSQLの発行回数は2回で済む。
(参考:Railsガイド)