🐕

【Rails】N+1問題をicludesで解決することについてまとめてみた

2023/07/01に公開

はじめに

現在、Railsについて絶賛学び中のコーダーです。学習の中で「N+1問題」という、名前からして難しそうなことが出てきたので、まとめてみようと思います。(間違いあったらごめんなさい)

N+1問題とは

データベースからデータを取得する際に、頻繁に発生するパフォーマンス系の問題です。
具体的には、アソシエーションを使用して、1つのオブジェクトに複数のオブジェクトが紐づいている場合に、(例として、ユーザーが複数の投稿を持っているとします。)ユーザーごとにその投稿を取得しようとすると、ユーザーの数だけデータベースへのクエリ(データベースへの処理要求)が発生します。
このことをN+1問題といいます。
N+1問題が発生すると、データベースからのデータ取得が非効率になり、アプリケーションのパフォーマンスが大きく低下してしまいます。

例のコードを用いて説明

  • Userモデル
app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end
  • Postモデル
app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

上記のように、UserモデルにPostモデルがアソシエーションにより紐づいているとします。(1対多の関係)
投稿一覧ページなどで、全てのユーザーとユーザーに紐づいた投稿を取得する場合に、以下のようなコードを書くことができます。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @users = User.all
  end
end
app/views/posts/index.html.erb
<% @users.each do |user| %>
  <h1><%= user.name %>の投稿</h1>
  <% user.posts.each do |post| %>
    <p><%= post.title %></p>
  <% end %>
<% end %>

上記のコードでは、各ユーザーの投稿を表示するたびに、そのユーザーに紐づく投稿データ(post)をデータベースから取得するクエリが実行されます。
よって、もしユーザーが10人いると仮定すると、クエリの回数は1(全ユーザー取得)+10(各ユーザーの投稿取得)=11回になり、N+1問題が発生します。

これを解決するのがincludesメソッドになります。

includesメソッドを使用した解決策

includesメソッドを使用することで、必要な全てのデータを一度にロードすることができます。

app/controllers/posts_controller.rb
  def index
    @users = User.includes(:posts).all
  end
end

上記のコードでは、Userをロードする時に、同時にPostも一緒にロードします。その結果、クエリの回数が、ユーザー全員取得全ユーザーの投稿取得の2回になります。
これにより、クエリの回数を大きく減らすことができ、パフォーマンス低下を防ぐことができます。

最後に

他にも「N+1問題」の解決方法がありそうですし、モデル同士の関係が「1対多」だけでなく、「多対多」の時などにはどうなるのか、などなどまた色々調べてみようと思います。

Discussion