N+1問題
N+1問題とは
SQLが必要以上に実行されてしまうとパフォーマンスが落ちるという問題です。
日常生活で例えるなら、お会計の時に品物をまとめて買うのではなく、
一つずつお金を払って、買っているような効率の悪いイメージです。
N+1問題の例
UserモデルとTweetモデルを例にして説明していきます。
ユーザーはたくさんのTweetをしているはずですよね。
なのでUserとTweetの関係は一対多の関係になっています。
それぞれのモデルを以下のように書き換えます。
# Userモデル class User < ApplicationRecord has_many :tweets end
# Tweetモデル class Tweet < ApplicationRecord belongs_to :user end
次にTweetをしたUserの名前を一覧画面に表示していこうと思います。
まず初めにTweetsコントローラを編集していきます。
def index @tweets = Tweet.all end
そして一覧画面のビューを生成します。
<% @tweets.each do |tweet| %> <%= tweet.user.name %> # Tweetモデルでアソシエーションを組んでいるのでuserをメソッドとして使える。 <% end %>
このコードだとまずはじめにTweetをすべて取得し、
その後各Tweetに対してusersテーブルから名前の情報を取得しています。
# Tweet.allの実行 SELECT 'tweets'.* FROM 'tweets' # tweet.user.name を実行する際に走る SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=1 SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=2 SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=3 SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=4
このようにtweetの数だけ
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id' = n
が発行されてしまいます。
N+1問題とは、はじめの1回のSQLでTweetモデルを取得し、
そのモデルに対するデータの数(N回)のSQLが実行されてしまうことを言います。
順番から考えると、N+1問題よりも1+N問題と呼んだ方がイメージしやすいと思います。
解決方法
includesメソッドを使って定義します。
Tweetsコントローラを編集します。
def index @tweets = Tweet.all.includes(:user) end
SELECT 'tweets'.* FROM 'tweets' # tweetに関連するユーザーの情報をまとめて取得してくれます。 SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id' IN (1, 2, 3, ......)
この上記の二つの SQL が発行されるだけになり、N+1 問題が解決します。