n+1問題とは
2つのテーブルがあってアソシエーションによって関連させている時、データの読み込みを行うとSQL文が余分に発行されてしまう現象で、大量のプログラムをアプリが所有していると読み込みに時間がかかってしまう問題のこと。
例えば、usersテーブルとtweetsテーブルの2つがありそれぞれが関連状態にあります。
モデルに記載された関係は、下のコードになります。
#userはtweetsに対して1対多の関係
has_many :tweets
#tweetはuserに対して多対1の関係
belongs_to :user
詳しくはアソシエーションを参考にしてください。
以下のプログラムが動作し、データベースからデータを取り出しHTMLに変換するとします。
<% @tweets.each do |tweet| %>
<div class="content_post">
<%= simpleformat(tweet.text) %>
</div>
<% end %>
すると、SQLでは以下のような処理が実行される。
# tweetsテーブルの全てのツイートデータを読み込む
SELECT `tweets`. * FROM `tweets`
#usersテーブルの全てのカラムからidが1のデータを取ってくる
SELECT `users`. * FROM `users` WHERE `users`.`id`=1
#ここが余分↓↓
SELECT `users`. * FROM `users` WHERE `users`.`id`=1
7行目の呼び出しが余分な箇所になります。
今回は、ツイートが少ない為、SQLの発行も少ないですが、ツイートデータが溜まってくると動作が遅くなる原因になってしまいます。
解決方法
どのように解決するのかと言うと、便利なメソッドがあります。
includesメソッド
指定した関連モデルをまとめて取得し、SQLの発行回数を減らすことができます。
今回の使い方だと、アソシエーションによってuserとtweetsが1対多の関係なのでtweetsを読み込むと同時にusersも読み込むようにすることができます。
このメソッドをtweetsコントローラーに記述します。
def index
@tweets = Tweet.includes(:user)
end
上のように記述するとn+1問題が解決します。
SQLはこのように変化します。
# tweetsテーブルの全てのツイートデータを読み込む
SELECT `tweets`. * FROM `tweets`
#usersテーブルの全てのカラムからidが1のデータを取ってくる
SELECT `users`. * FROM `users` WHERE `users`.`id`=1
先ほどよりも1文少なくなっています。
その理由は、テーブルのデータを同時に読み込んでいるからです。
SQL文をこうやって見ると「n+1問題」よりも「1+n問題」の方が私はしっくりきました。
まとめ
- n+1問題とはSQL文を余計に発行してしまう現象のこと
- includesメソッドで解決できる