個人メモです。
eachやmapなど繰り返し処理をする中で、SQLクエリを投げるのは禁止。(稀にSQLを投げた方が早い場合もあるそう、、)
eachという早い処理の中で、短時間に何度もDBにアクセスすることになる。要素の数が1000個あったら1000回DBに接続することになる。
この問題を総じてN+1問題と呼ぶ。どういった場合がNGでどう対処したらいいかと、N+1問題とはそもそもどういう意味かについて。
## 目次
- NG事例(これはやってはダメ!)
- 対処法
- [preload, includes, eager_loadの違い](#preload_ includes_eager_loadの違い)
- N+1問題とは?
NG事例(これはやってはダメ!)
# Userモデルでデータ取得
users = ::User.all
user_names = []
users.each { |user|
#UserProfileモデルでデータ取得
user_names << ::UserProfile.find_by(user_id: user.id).name
}
<<
は配列の末尾に指定したデータを入れるメソッド(pushメソッドみたいなもの)。
usersの要素の数だけ、::UserProfile.find_by(user_id: user.id)
でSQLクエリを投げる処理が繰り返されてしまう。
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 1 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 2 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 3 LIMIT 11
.
.
・
関連付けられたDBの場合
has_one
やhas_many
で関連付けられたモデルの場合、省略して表記できるが、同じくSQLを投げてしまうのでNG。
▼例
UserモデルとUserProfileモデルを関連付けた場合。
class User < ApplicationRecord
has_one :user_profile
end
class UserProfile < ApplicationRecord
belongs_to :user
end
この時、Userモデルを呼び出して、.user_profile
を付ければ、UserProfileの関連するデータを呼び出せる。
users = ::User.where(medium_id: 34)
user_name_arr = []
users.each { |user|
#Userモデルに.user_profleをつけるだけ
uesr_name_arr << user.user_profile.name
}
モデル冒頭の
::
は名前空間の中にない(最上位の階層)ことを明示している。(バグ防止目的。基本的には、Userも::Userも同じ処理結果になる)
この場合でもSQLが投げられるので、user.user_prifile
の度に以下のSQL処理が実行される。
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 1 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 2 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 3 LIMIT 11
・
・
・
DBの負荷が上がってしまうためこれも避けなくてはいけない。
**※関連付けモデルの注意点** 関連づけられたモデルを指定するためには、データを絞り込む必要がある。例えば、allメソッドは絞り込みしていないので、`User.all.user_profile`は取得できない。
User.first.user_profile
やUser.find_by(user_id: 7).user_profile
など、絞り込みした後であれば問題なく動く。
## 対処法 SQLを投げる回数を最小限に抑えるために、eachでループ処理を実行する前にDBからデータを取得しておく。
関連づけられていないモデルの場合
# Userモデルでデータ取得
user_profs = ::UserProfile.all
user_names = []
user_profs.each { |user|
user_names << user.name
}
### 関連づけられたモデルの場合 関連付けられたモデルの場合は`include(:関連づけたモデル名)`を使う。
▼実例
Userモデルにhas_one :user_profile
が関連づけられている場合の例。
user_profiles = ::User.where(medium_id: 34).includes(:user_profiles)
user_name_arr = []
user_profiles.each { |user_profile|
uesr_name_arr << user_profile.name
}
**実行されるSQLクエリ**
SELECT "users".* FROM "users" WHERE "users"."medium_id" = 34 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."media_user_id" IN (6, 7)
usersテーブルと、user_profilesテーブルのそれぞれからデータを取得するため、SQLは2回実行される。
これで必要なデータの取得は完了するので、あとはeachで取り出すだけ。
## preload, includes, eager_loadの違い 関連するテーブルを読み込む方法は、includesの他にpreloadとeager_loadがある。
preloadとeager_loadはクエリを複数投げるかどうか、絞り込みができるができないかといった挙動の差がある。
includesは状況に応じてpreloadとeager_loadの処理を切り分けるので、基本的にはincludesを使っておけば問題ない。
## N+1問題とは? **余計なSQLを実行しているという問題**。処理速度は遅くなるし、DBアクセス回数が増えて負荷が上がるというデメリットしかない。
例えば、ある会員の情報が入ったUserモデルから、会員一人一人の情報を抜き出す場合に、SQLを投げる方法がいくつかある。
(1)会員数の数だけSQLを投げる(会員全員のデータを取得済み)
・会員全員のデータ取得のためのSELCT文を投げる(1回)
・SELECTが会員数の数N会投げられる。(N回)
(2)会員数の数だけSQLを投げる。(全会員データはなし)
・SELECTが会員数の数N会投げられる。(N回)
(3)全会員のデータを取得してループで回す
・会員全員のデータ取得のためのSELCT文を投げる(1回)
(1)から順番に処理負荷が高く、コードも冗長になっている。この冗長なコードで**投げるSQLクエリの回数がN+1になっているので、N+1問題と呼ぶ**。
なお、(2)はN問題で、N+1より僅かにマシだが、ほぼ変わらない。