現在プログラミング勉強中です。
N+1問題について勉強していて、イメージがつき辛かったので備忘録としてまとめてみます。
#N+1問題とは
N+1問題とは、アソシエーションの関係にあるモデル間で、データベースへのアクセス回数が膨大になってしまうというものです。
例えばTwiiterなどでは、以下の画像のようにtweet毎に投稿者の名前が表示されています。
このようなtweet毎に紐づくユーザーネームを取ってくるためには、tweetの数分、usersテーブルにアクセスする必要があります。
具体的にコードを確認します。
まずは事前設定として、以下の表のようにアソシエーションの関係にある、
tweetsテーブルとusersテーブルを設定します。
このアソシエーションで関連づいているモデルで、tweetに紐づくuser_nameを全て取得します。
この時N+1問題を考慮せずに記述すると、
tweets = Tweet.all
tweets.each do |tweet|
tweet.user.name
end
となります。
しかしこのように記述すると、each文の中でtweetの数だけ(N回)、userモデルにアクセスをしてしまいます。
実際に実行して、SQL文を見てみます。
tweets = Tweet.all
#TweetモデルへのSQLが発行されている
Tweet Load (1.2ms) SELECT `tweets`.* FROM `tweets`
まず全てのtweetを取得する時にTweetモデルにアクセスしています。
そして次に、
tweets.each do |tweet|
tweet.user.name
end
# tweetの回数分(4回)UserテーブルにSQLが発行されている
User Load (1.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
User Load (0.6ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1
このように、each文の中でtweetの数だけUserテーブルにアクセスしている事がわかります。
今回の例では4回なので影響は少ないですが、
tweetが1000回、1万回となるとアクセス数も膨大になってしまいます。
#includesメソッド
このようなN+1問題を解決するために、includesメソッドを使うことで関連モデルへのアクセスを一度で行う事ができます。書き方としては
モデル名.includes(:関連モデル名)
です。
例えば以下のように全てのtweetを取得する際に、allメソッドではなく、includesメソッドを定義することができます。
tweets = Tweet.includes(:user)
tweets.each do |tweet|
tweet.user.name
end
上記の例では、全てのtweetを取得する際に、同時に紐づいたuserも参照しています。そのために、続くeach文では、N回というuserモデルへのアクセスが不要になります。
実際に実行してSQL文を見てみます。
tweets = Tweet.includes(:user)
#Tweetモデルと、UserモデルへのSQLは合計で2つだけ
Tweet Load (1.0ms) SELECT `tweets`.* FROM `tweets`
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4)
すると
Tweetモデルへのアクセスを行うと同時に、紐づく全てのUserモデルへのアクセスも1回で行えている事がわかります。
(この時、続くeach文ではSQLは発行されません)
従ってincludesメソッドを使う事で、
N回というどんなにtweetの数が膨大でも、データベースへのアクセスとしては合計2回で済むのです。
#まとめ
N+1問題とは、アソシエーションがあるモデルで、N回という膨大なデータベースへのアクセスをしてしまうことです。
解決策として、includesメソッドを使う事でモデルへのアクセスを2回で済ます事ができます。