1
1

More than 1 year has passed since last update.

N+1問題の直感的理解

Last updated at Posted at 2022-02-19

N+1問題

例えば、user(has_many :posts)なるテーブルと、post(belongs_to :user)なるテーブルの二つがあるとする。
userは0個以上のpostを持っていて、postには各userのid(user_id)が必ず対応している。
ここで、各userの持っているpostを取り出し、userの名前(user.name)と、postのタイトル(post.content)を全件表示したいとき、どのようなコードを書けばよいだろうか。

最も分かりやすいのは以下のコードだろう。

view
User.all.each do |user|
  user.posts.each do |post|
    puts "#{user.name} / #{post.title}"
  end
end

このコードで実行される内容は、

(1). userの情報(user.id,user.name)を取得するために、userテーブルにアクセス
2. id = 1のuserのpostの情報を取得するためにpostテーブルにアクセス
3. id = 2のuserのpostの情報を取得するためにpostテーブルにアクセス
4. id = 3のuserのpostの情報を取得するためにpostテーブルにアクセス
5. id = 4のuserのpostの情報を取得するためにpostテーブルにアクセス

...

n+1. id = nのuserのpostの情報を取得するためにpostテーブルにアクセス

とまあこんな感じで、n+1回の処理が行われることになる。
しかし、n+1回も処理してたら重いし問題だよねということで、このような状況が「N+1問題」と呼ばれる。

解決策

以下のようにcontroller内でincludesメソッドを使ってインスタンス変数を定義しておくことは、ポピュラーな解決策の一つ。

controller
@users = User.includes(:posts)
view
@users.each do |user|
  user.posts.each do |post|
    puts "#{user.name} / #{post.title}"
  end
end

このコードで実行される処理は

  1. userの情報を取得するために、userテーブルにアクセス
  2. postの情報を取得するためにpostテーブルにアクセス
  3. userのidと、postに関連付けられているuser_idを結びつけてテーブル同士を結合
  4. 結合されたテーブルの情報を基にviewにuserの名前とpostのタイトルが表示される

と、テーブルへのアクセスが最初の2回のみに抑えられている。

先程の例との違いは、一回一回データを取りにテーブルを出入りするのではなく、userデータとpostデータ、それぞれ一回でそのテーブルの全てのデータをまとめて取得しておいて、user_idというキーでテーブル同士を結びつけ、順番に表示する点。

また、テーブルをJOINしてから取得する方法も解決策として考えられる。
includeとJOINの違いは、includeがuserのデータと、postのデータをそれぞれ別々に取得して、アプリケーションでそれらを紐づけていくのに対して、JOINの場合はデータを取得する段階で、userとpostを結びつけた状態でデータを取得する。このため、includeの場合は発行されるクエリは2回であるのに対し、JOINの場合は発行するクエリは1回のみ。ただし、データを結びつけた状態で取得するJOINの処理には時間がかかる。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1