学習記録

アウトプット用に作りました

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回のSQLTweetモデルを取得し、
そのモデルに対するデータの数(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 問題が解決します。