【Ruby on Rails】N＋1問題対策（preload、eager_load、includes）

# はじめに

Railsなどを中心に勉強中のエンジニア初心者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。
間違っていることもあると思われるので、その際は指摘いただけると幸いです。

# N＋1問題とは

N＋1問題とは、データベースからデータを取り出す際に、大量のSQLが発行されて動作が遅くなる問題のこと。

もう少し具体的に言うと、ループ処理の中で都度SQLを発行してしまうことで、1つ目の値の関連データを取得するためにSLQを発行し、2つ目の値の関連データを取得するためにSQLを発行し、3つ目の、、、というイメージ。

関連データもまとめて取得してからループ処理に入ればN＋1問題は起きない（はず）。

# N＋1問題の例

`Room`モデルと`User`モデルがそれぞれ関連づけられており、各`room`に紐づく`user.name`を取得したいとする。

### コントローラとビュー

```ruby
# コントローラ
@rooms = Room.all
```

```ruby
# ビュー
@rooms.each do |room|
	room.user.name
end
```

### N＋1問題の発生

コントローラで`room`一覧を取得する際のSQL。

```bash
Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
```

ビューで`room.user.name`を表示する際のSQL文。

各`room`毎に紐づいている`user`を都度取得しているため、`room`の数が増えるほど発行されるSQLも増えてしまう。

```bash
User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
```

# N＋1問題対策

N＋1問題の対策として、以下のメソッドを使った対策が挙げられる。

以下は、アソシエーションされたモデルのデータを効率よく取得するためのメソッドである。

## preloadメソッド

`preload`メソッドは2つのクエリを発行する。

まずは、`room`一覧を取得するSQLが発行され、その次にその`room`に紐づく`user`を一括で取得するSQLが発行されている。

```ruby
# コントローラ
@rooms = Room.all.preload(:user)
```

```bash
Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]
```

## eager_loadメソッド

`eager_load`引数に取ったマスターテーブルとして外部結合する。

今回は`users`テーブルをマスターテーブルとして、`rooms`テーブルと外部結合（`left outer join`）している。

```ruby
# コントローラ
@rooms = Room.all.eager_load(:user)
```

```bash
SQL (0.2ms)
SELECT
	"rooms"."id" AS t0_r0,
	"rooms"."room_name" AS t0_r1,
	"rooms"."room_introduction" AS t0_r2,
	"rooms"."usage_fee" AS t0_r3,
	"rooms"."address" AS t0_r4,
	"rooms"."room_picture" AS t0_r5,
	"rooms"."created_at" AS t0_r6,
	"rooms"."updated_at" AS t0_r7,
	"rooms"."user_id" AS t0_r8,
	"users"."id" AS t1_r0,
	"users"."name" AS t1_r1,
	"users"."email" AS t1_r2,
	"users"."password_digest" AS t1_r3,
	"users"."created_at" AS t1_r4,
	"users"."updated_at" AS t1_r5,
	"users"."icon_img" AS t1_r6,
	"users"."introduction" AS t1_r7
FROM "rooms" LEFT OUTER JOIN "users" ON "users"."id" = "rooms"."user_id"

```

## includesメソッド

デフォルトでは、`preload`と同様の挙動を示す。

関連先のテーブルで絞り込みを行っている場合などは、`eager_load`と同じ挙動になるとのこと。

いろいろな記事を調べていると、`preload`と`eager_load`を使って挙動を明確にすることを推奨している記事が多い印象。

```ruby
# コントローラ
@rooms = Room.all.includes(:user)
```

```bash
Room Load (0.2ms)  SELECT "rooms".* FROM "rooms"
User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]
```

# 参考

https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361

https://qiita.com/seiyatakahashi/items/37bef984168b9f1ce25e

https://qiita.com/mball/items/5d4228cd3523f7a1ad04

https://tech.stmn.co.jp/entry/2020/11/30/145159

# 最後に

いかがでしたでしょうか。
ここ違うよ！でしたり、こうした方がいいよ！などがあればコメントいただけると幸いです。

他にも下記のような記事を投稿しております。
興味がありましたら、ぜひご覧ください。

https://qiita.com/sekkey_777/items/7907f7d0f46fd6eaad9d

https://qiita.com/sekkey_777/items/b40135995de01ae95dc2