N+1問題の解決方法（初心者向け）

## 環境

Ruby 2.7.3

Rails 6.1.4

## N+1問題とは

**必要以上にSQLが発行されてしまい、パフォーマンスが悪くなる問題のこと**。


実際にN＋1問題の例を以下に示す。
シンプルに`Userカラム`と`Groupカラム`が中間テーブル`GroupUser`を跨いで多対多の関係になっている。


![Database ER diagram (crow's foot) - Database ER diagram (crow's foot).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/125dc7d0-9a5e-1cfd-b963-9c9c3c3ea46b.png)

N＋1問題について考えず、各GroupのUserに対してある処理をしたい。
以下のようにUserデータを取得するために、随所随所でSQLが発行されておりパフォーマンスが悪い。

問題のあるコード

```ruby
Groups.each do |group|
  group.users.each do |user|
    id = user.id
    pp id
  end
end

---以下 実行内容---

Group Load (26.8ms)  SELECT `group`.* FROM `group` 
User Load (17.2ms)  SELECT `user`.* FROM `user` INNER JOIN `group_user` ON `user`.`user_id` = `group_user`.`user_id` WHERE `group_user`.`group_id` = 97
"sample37"
"sample26"
"sample41"
"sample54"
User Load (16.5ms)  SELECT `user`.* FROM `user` INNER JOIN `group_user` ON `user`.`user_id` = `group_user`.`user_id` WHERE `group_user`.`group_id` = 98
  # ↑の処理が随所で出てくるのはよろしくない
"sample36"
"sample12"
"sample4"
"sample49"
User Load (15.3ms)  SELECT `user`.* FROM `user` INNER JOIN `group_user` ON `user`.`user_id` = `group_user`.`user_id` WHERE `group_user`.`group_id` = 99
"sample2"
"sample51"
"sample48"
"sample24"
```

## 解決方法

・includes メソッド（極力使わない）
・preloadメソッド
・eager_loadメソッド

**３種類あるけど、includeメソッドは極力使わず、preload, eager_load のどちらかに**
**絞りたい。includesがダメなわけではないが、思考停止でincludesに頼るのは良くない。**

▼以下記事を参照▼
[ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由](https://qiita.com/ryosuketter/items/097556841ec8e1b2940f#includes)
> includesは利用しない方が良いでしょう。
なぜなら、includesは、preloadとeager_loadをよしなに振り分けるので。
preloadと、eager_loadの特徴を理解していれば、includesが登場する場面は、
ほとんどないと思います。データが少ないうちはincludesしていても問題にならないかもしれませんが、
データが増えてきたときにジワジワと問題が顕在化してくるので、includesの挙動も正しく知って
おきましょう。リファクタの際に役立つと思います。
>

**使い分けとしては、関連先のテーブルデータを**

**・参照した絞り込みを行う場合：eager_load**
**・参照せず、アソシエーションを取得する場合：preload**

では、実際にそれぞれのメソッドでどのようなSQLが走るかを確認します。


## preload メソッドを使用した場合

実は、`includesメソッド`と全く同じSQLを発行することがわかる。
つまり、今回の例では`includesメソッド`の中で`preload`が選択され、実行されている。

```ruby
Groups.preload(:users).each do |group|
  group.users.each do |user|
    id = user.id
    pp id
  end
end

--- 以下 実行結果 ---

Group Load (225.8ms)  SELECT `group`.* FROM `group` WHERE `group`.`ttt_status` = 'checking'
GroupUser Load (12.3ms)  SELECT `group_user`.* FROM `group_user` WHERE `group_user`.`group_user_id` IN (97, 98, 99, 100, 101)
User Load (12.6ms)  SELECT `user`.* FROM `user` WHERE `user`.`user_id` IN (39, 28, 43, 56, 38, 14, 5, 51, 3, 53, 50, 26, 1, 42, 40, 18, 29, 45, 20, 23)
"sample37"
"sample26"
"sample41"
"sample54"
"sample12"
"sample4"
"sample49"
"sample2"
"sample51"
"sample48"
"sample24"
```

## eager_load メソッドを使用した場合

`name`や`slack_id`などのアソシエーション先である`User`カラム情報も取得している。
そして、特徴的なのは**１回のクエリで関連先のデータを取得していること。**
そのため、関連先のモデルデータで絞り込みができる。

```ruby
Groups.eager_load(:users).each do |group|
  group.users.each do |user|
    id = user.id
    pp id
  end
end

--- 以下 実行結果 ---

SQL (168.2ms)  SELECT `group`.`group_id` AS t0_r0,
`group`.`event_date` AS t0_r1, `group`.`group_name` AS t0_r2,
`group`.`confirm_flag` AS t0_r3, `group`.`status` AS t0_r4,
`group`.`delete_flag` AS t0_r5, `group`.`deletion_datetime` AS t0_r6,
`group`.`registration_datetime` AS t0_r7, `group`.`update_datetime` AS t0_r8,
`group`.`update_timestamp` AS t0_r9,
`user`.`user_id` AS t1_r0,
`user`.`name` AS t1_r1, `user`.`slack_id` AS t1_r2, `user`.`image_url` AS t1_r3,
`user`.`email` AS t1_r4, `user`.`recess_flag` AS t1_r5,
`user`.`delete_flag` AS t1_r6, `user`.`deletion_datetime` AS t1_r7,
`user`.`registration_datetime` AS t1_r8, `user`.`update_datetime` AS t1_r9,
`user`.`update_timestamp` AS t1_r10
FROM `group` LEFT OUTER JOIN `group_user`
ON `group_user`.`group_id` = `group`.`group_id`
LEFT OUTER JOIN `user` ON `user`.`user_id` = `group_user`.`user_id`
WHERE `group`.`status` = 'checking'

"sample37"
"sample26"
"sample41"
"sample54"
"sample12"
"sample4"
"sample49"
"sample2"
"sample51"
"sample48"
"sample24"
-----補足-----

以下のように、連結先のテーブルの条件で絞り込みをしたい時に使える。
※Group テーブルに GroupUser テーブルを連結し、その GroupUserにおいて、status='present'で絞る

Group.eager_load(:group_user).where(group: {status: 'present'})
```

## 結論

N＋1問題を解決するために`includes メソッド`、`preloadメソッド`、`eager_loadメソッド`
を使用する。`includes メソッド`**は極力使わず、やりたいことに合わせて**`preloadメソッド`、`eager_loadメソッド`で取捨選択する。

## 参考

[ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い](https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58#includes)

[ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由](https://qiita.com/ryosuketter/items/097556841ec8e1b2940f)

[ActiveRecordのincludes, preload, eager_load の個人的な使い分け](https://moneyforward.com/engineers_blog/2019/04/02/activerecord-includes-preload-eagerload/)