【Rails】ループ処理（each文など）の中でSQLクエリを投げてはいけない。n+1問題とは何かと解決法

個人メモです。

eachやmapなど繰り返し処理をする中で、**SQLクエリを投げるのは禁止**。（稀にSQLを投げた方が早い場合もあるそう、、）

eachという早い処理の中で、短時間に何度もDBにアクセスすることになる。要素の数が1000個あったら1000回DBに接続することになる。

この問題を総じてN＋1問題と呼ぶ。**どういった場合がNGでどう対処したらいいかと、N＋１問題とはそもそもどういう意味かについて**。


<br>
##目次

1. [NG事例（これはやってはダメ！）](#ng事例これはやってはダメ)
2. [対処法](#対処法)
3. [preload, includes, eager_loadの違い](#preload_ includes_eager_loadの違い)
4. [N+1問題とは？](#n1問題とは)



##NG事例（これはやってはダメ！）

```rb: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クエリを投げる処理が繰り返されてしまう。


```:繰り返し実行される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モデルを関連付けた場合。

```rb:user.rb（モデル）
class User < ApplicationRecord
  has_one :user_profile
end
```

```rb:UserProfile.rb（モデル）
class UserProfile < ApplicationRecord
  belongs_to :user
end
```

この時、Userモデルを呼び出して、`.user_profile`を付ければ、UserProfileの関連するデータを呼び出せる。

```rb:コントローラの処理
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の負荷が上がってしまうためこれも避けなくてはいけない。


<br>
**※関連付けモデルの注意点**
関連づけられたモデルを指定するためには、データを絞り込む必要がある。例えば、allメソッドは絞り込みしていないので、`User.all.user_profile`は取得できない。

`User.first.user_profile`や`User.find_by(user_id: 7).user_profile`など、絞り込みした後であれば問題なく動く。



<br>
##対処法
SQLを投げる回数を最小限に抑えるために、eachでループ処理を実行する前にDBからデータを取得しておく。


###関連づけられていないモデルの場合

```rb:OK
#Userモデルでデータ取得
user_profs = ::UserProfile.all

user_names = []
user_profs.each { |user|
      user_names << user.name
 }
```


<br>
###関連づけられたモデルの場合
関連付けられたモデルの場合は`include(:関連づけたモデル名)`を使う。


**▼実例**
Userモデルに`has_one :user_profile`が関連づけられている場合の例。

```rb:コントローラの処理
user_profiles = ::User.where(medium_id: 34).includes(:user_profiles)

user_name_arr = []
user_profiles.each { |user_profile|
  uesr_name_arr << user_profile.name
}
```

<br>
**実行される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は２回実行される。

これで必要なデータの取得は完了するので、あとはeachで取り出すだけ。



<br>
##preload, includes, eager_loadの違い
関連するテーブルを読み込む方法は、includesの他にpreloadとeager_loadがある。

preloadとeager_loadはクエリを複数投げるかどうか、絞り込みができるができないかといった挙動の差がある。

includesは状況に応じてpreloadとeager_loadの処理を切り分けるので、基本的にはincludesを使っておけば問題ない。



<br>
##N+1問題とは？
**余計なSQLを実行しているという問題**。処理速度は遅くなるし、DBアクセス回数が増えて負荷が上がるというデメリットしかない。

例えば、ある会員の情報が入ったUserモデルから、会員一人一人の情報を抜き出す場合に、SQLを投げる方法がいくつかある。

**(1)会員数の数だけSQLを投げる（会員全員のデータを取得済み）**
・会員全員のデータ取得のためのSELCT文を投げる（1回）
・SELECTが会員数の数N会投げられる。（N回）


**(2)会員数の数だけSQLを投げる。（全会員データはなし）**
・SELECTが会員数の数N会投げられる。（N回）


**(3)全会員のデータを取得してループで回す**
・会員全員のデータ取得のためのSELCT文を投げる（1回）


<br>
(1)から順番に処理負荷が高く、コードも冗長になっている。この冗長なコードで**投げるSQLクエリの回数がN+1になっているので、N+1問題と呼ぶ**。

なお、(2)はN問題で、N+1より僅かにマシだが、ほぼ変わらない。