【Ruby on Rails】n+1 問題とは? 



## 目次
- はじめに
- n+1問題とは
- n+1を解決する方法
  - preload
  - eager_load
  - includes
  - どれを使うのが良いのか？

---

## はじめに
Ruby on RailsではActiveRecordというORMを採用しているため、自身でクエリ文を書かずともデータベースからデータを取得することが可能です。

例えば、あるモデルの全てのデータを取得したいとします。以下のように書けば、望んだデータの取得ができます。

```ruby
Model.all
```

これを、SQLで書くと以下のようになります。

```shell
SELECT
  "model".*
FROM
  "models"
```

これでもまだシンプルな方ではあります。実際には複数の条件を指定して、複数のテーブルと関連づいたデータモデルを扱わなければなりません。そうなると、上記のようにシンプルなクエリ文では済まなくなってきます。

ActiveRecordはこんな感じでよしなにデータを取得してきてくれるのですが、何も考えずに使用していくと、これから解説する`n+1問題`が発生する可能性が大いにあるため、しっかり理解して使用する必要があります。

## n+1問題とは
簡潔に言うならば、「余計なクエリが発行される」問題のことです。
余計なクエリが発行されるということは、アプリ自体のパフォーマンスが低下することになります。

もっと具体的に解説していきます。

`会員テーブル（users）`と`投稿テーブル（posts）`があったとして、これらは、`1対多`で関連づいています。

![](https://outsource-systems.com/content/images/2021/04/user-post-er.png)

投稿一覧画面において、投稿に紐づくユーザーを取得する場合があるとします。

```ruby
# posts_controller.rb
def index
  @posts = Post.all # DBに保存されている全ての投稿を取得する
end

# views/posts/index.html.erb
# 投稿のタイトルと投稿に紐づくユーザー名を表示する
@posts.each do |post|
  post.title
  post.user.name
end
```

このようなコードを書くと以下のようなクエリが発行されます。

```shell
Post Load (0.2ms)  SELECT "posts".* FROM "posts"

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

- 投稿テーブルの全データを取得
`Post Load (0.2ms)  SELECT "posts".* FROM "posts"`

- 投稿に紐づくユーザーデータを取得
`User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", x], ["LIMIT", 1]]`


このように`投稿に紐づくクエリ`が`１回`、`投稿ごとユーザーを取得するクエリ`が`n回`生成されてしまうこととなります。

なぜこのような問題が発生するかというと、データモデル間（users-posts）に`has_many`や`belongs_to`といったアソシエーションが組まれているためです。

## n+1問題を解決する方法

RubyonRailsにはn+1を解決するための便利なメソッドが用意されています。

- preload
- eager_load
- includes

### preload

preloadは`2つ`のクエリを発行します。
一つは投稿（posts）を取得するクエリ、もう一つは関連づいている会員（users）を取得するクエリです。

```ruby
def index
  @posts = Post.preload(:user)
end
```

```shell
Post Load (30.8ms)
SELECT
    "posts".*
FROM
    "posts"

User Load (0.2ms)
SELECT
    "users".*
FROM
    "users"
WHERE
    "users"."id" IN(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [[nil, 2], [nil, 5], [nil, 8], [nil, 4], [nil, 9], [nil, 6], [nil, 7], [nil, 3], [nil, 10], [nil, 1]]
```

### eager_load

eager_loadは引数にとったテーブルを左外部結合（LEFT OUTER JOIN）します。

[LEFT OUTER JOINとは](https://www.dbonline.jp/sqlite/join/index2.html#section2)

prelaodとは違うクエリになっているのがわかると思います。

```ruby
def index
  @posts = Post.eager_load(:user)
end
```

```shell
SELECT
    "posts"."id" AS t0_r0,
    "posts"."title" AS t0_r1,
    "posts"."body" AS t0_r2,
    "posts"."user_id" AS t0_r3,
    "posts"."created_at" AS t0_r4,
    "posts"."updated_at" AS t0_r5,
    "users"."id" AS t1_r0,
    "users"."name" AS t1_r1,
    "users"."email" AS t1_r2,
    "users"."created_at" AS t1_r3,
    "users"."updated_at" AS t1_r4
FROM
    "posts"
    LEFT OUTER JOIN
        "users"
    ON  "users"."id" = "posts"."user_id"
```

eager_loadは投稿テーブルと会員テーブルをJoinしているため、一回のクエリで済んでいます。

一回のクエリでデータを取得してくるので、一見`preload`よりもパフォーマンスが高いと考える方もいるかもしれませんが、取得してくるデータ量が多ければ多いほど、パフォーマンスは低下します。

### includes

includesは`preload`と`eager_load`をよしなに切り替えてくれるメソッドになります。
今回の場合はpreloadと同じ挙動になっています。

```ruby
def index
  @posts = Post.includes(:user)
end
```

```shell
Post Load (0.2ms)  SELECT "posts".* FROM "posts"

User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)  [[nil, 10], [nil, 8], [nil, 1], [nil, 3], [nil, 7], [nil, 5], [nil, 4], [nil, 6], [nil, 9], [nil, 2]]
```

では、eager_loadと同じ挙動になる場合はどのような場合でしょうか？

includesしたテーブルでwhere句などを使用して条件を絞った場合にeager_loadと同じ挙動（LEFT OUTER JOIN）になります。

```ruby
def index
  @posts = Post.includes(:user).where(user: { id: 1 }) # 会員IDが1の投稿のみを取得する
end
```

```shell
SELECT
    "posts"."id" AS t0_r0,
    "posts"."title" AS t0_r1,
    "posts"."body" AS t0_r2,
    "posts"."user_id" AS t0_r3,
    "posts"."created_at" AS t0_r4,
    "posts"."updated_at" AS t0_r5,
    user."id" AS t1_r0,
    user."name" AS t1_r1,
    user."email" AS t1_r2,
    user."created_at" AS t1_r3,
    user."updated_at" AS t1_r4
FROM
    "posts"
    LEFT OUTER JOIN
        "users" user
    ON  user."id" = "posts"."user_id"
WHERE
    "user"."id" = ? [["id", 1]]
```

### どれを使うのが良いのか？

とりあえずincludesを書いておけば、Rails側で状況に応じて挙動が異なると思いますが、どう言ったケースでどういった挙動になるのかを理解せずに利用するのはあまりよろしくありません。

個人的には、コードを読む側にとってはpreloadとeager_loadを使い分けていた方が可読性は高いのかなと思います。

ですが、これはコードを書く人やプロジェクトごと認識が違うところではあるので、その時に合った使い方をしましょう。





## 記事引用元
https://outsource-systems.com/n-1about/