はじめに
Railsを学習していると、一度は耳にする「N+1問題」。
なんとなく
「クエリがいっぱい発行されてよくないやつ」
という理解はしていましたが、正直なところ仕組みをきちんと説明できませんでした。
そこで今回は、自分の理解を整理するためにも、N+1問題についてまとめてみます。
N+1問題とは
N+1問題とは、
本来最小限のクエリで済むはずのデータ取得が、不要に複数回のSQL発行になってしまう問題です。
例として、UserとPostが1対多の関係にあるとします。
# controller
@users = User.all
<% @users.each do |user| %>
<%= user.name %>
<% user.posts.each do |post| %>
<%= post.title %>
<% end %>
<% end %>
この場合、発行されるSQLは以下のようになります。
- usersを取得するクエリ(1回)
- 各userごとにpostsを取得するクエリ(N回)
つまり、
1(users取得) + N(posts取得) = N+1回
ユーザーが100人いれば、SQLは101回発行されます。
データ量が増えるほど、パフォーマンスが悪化していくのが問題です。
-
User.allでは users テーブルのデータしか取得していない -
user.postsを呼び出した時点で、初めて posts テーブルへのクエリが発行される - これがループの回数分(ユーザー数)繰り返されるため、N回のクエリになる
どう改善する?
Eager Loading(事前読み込み)
N+1問題が起きるのは Lazy Loading(必要になった時に読み込む) が原因です。
Railsでは includes を使うと Eager Loading(事前にまとめて読み込む) になります。
# N+1が発生する例
@users = User.all
# 改善例
@users = User.includes(:posts)
これにより、Railsは事前にpostsもまとめて取得します。
発行されるSQLは次のようになります。
- usersを取得(1回)
- 対象usersのpostsをまとめて取得(1回)
つまり、2回で済むようになります。
これを「Eager Loading(事前読み込み)」といいます。
- N+1問題が起きるのは Lazy Loading(必要になった時に読み込む)が原因
- includes を使うと Eager Loading(事前にまとめて読み込む)になる
他にもある?
Railsには以下のメソッドもあります。
- preload
- eager_load
違いはJOINを使うかどうかなどですが、基本的なN+1対策としては includes を覚えておけば十分だと思いました。
まとめ
N+1問題は、
- 関連データをループ内で取得することで起きる
- データ量が増えるほどパフォーマンスが悪化する
- includes を使うことで解決できる
という問題でした。
「とりあえずincludesをつける」ではなく、
なぜN+1が起きるのか、なぜincludesをつけるのかを理解することが大切
だと感じました。