##N+1問題とは
N + 1問題とは、必要以上にSQLが発行されてしまい、動作が悪くなってしまう問題のこと。
必要以上にSQLが発行されると時間が掛かり、これが数回なら特に動作に影響はないが、何万回と回数が増えると動作が重くなってしまう。
##N+1問題を実際に見てみる
※ここではテーブルのアソシエーションについての説明は省く。
ネコの飼い主のテーブルがownersテーブル、ネコの名前を扱うテーブルがcatsテーブルの2つのテーブルを例にN +1 問題を確認していく。
「全ての飼い主の猫の一覧」をviewで表示したい場合、controller側で全ての飼い主をallメソッドで取得し、view側で飼い主の持つcatsをアソシエーションによって下記の様に記述。
# controller
@owners = Owner.all
# view
@owners.each do |owner|
owner.cats.each do |cat|
cat.name
end
end
結果:
# controller
SELECT `owners`.* FROM `owners`
# view
SELECT `cats`.* FROM `cats` WHERE `cats`.`owner_id` = 1
SELECT `cats`.* FROM `cats` WHERE `cats`.`owner_id` = 2
SELECT `cats`.* FROM `cats` WHERE `cats`.`owner_id` = 3
SELECT `cats`.* FROM `cats` WHERE `cats`.`owner_id` = 4
その結果、catsテーブルに対して、4回のアクセスが行われている事がわかる。
このようにownersテーブルへのアクセス1回に対して、関連するテーブルがN回発行されている
1+Nの状況を「N+1問題」と言う。
この問題を解決すれば、ownersテーブルにアクセスする際に、関連するcatsテーブルのレコードを取得する事が出来れば、発行されるSQLも4回から1回にまとめる事が出来る。
##includesメソッドを使ってN+1問題を解決
includesメソッドで指定された関連付けが最小限のクエリ回数で読み込まれるため、これによってN+1問題を解決する事が出来る。
includesメソッド
includesメソッドは、引数にアソシエーションで定義した関連名を指定して定義。(※テーブル名ではない)
モデル名.includes(:関連名)
先程の飼い主と猫のテーブル例にincludesメソッドを入れると、viewのアクセス数が4回→1回
に減っている。
# controller
@owners = Owner.includes(:cats)
# view
@owners.each do |owner|
owner.cats.each do |cat|
cat.name
end
end
結果:
SELECT `owners`.* FROM `owners`
SELECT `cats`.* FROM `cats` WHERE `cats`.`owner_id` IN (1, 2, 3, 4...)
このように、includesメソッドを使うと関連するテーブルのレコードをまとめて取得させる事によって必要以上のSQLを発行する事なく済むようになる。