ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由

## はじめに

ActiveRecordでN+1問題やスロークエリを解消するためにeager loadingを行う場合、普段Railsを使って開発されている方であれば、パッと思いつくのは`includes`ではないでしょうか？もしくは、`preload`や`eager_load`を使用しますよね。

この記事では、Webサイト表示パフォーマンスを保つため、ActiveRecordのメソッドの違いや、どいういう場合に使ったら良いのか、どいういう場合には使わない方が良いのかについて書きました。

実際に Railsアプリケーションを作成して解説していきます。

### 用語の説明と分類「ORM の Eager loading と Lazy loading」

Webサイト表示パフォーマンスを保つため、ORM（RailsではActiveRecord）では、Eager loading と Lazy laodingというものをサポートしています。

#### Eager loadingとは

予めメモリ上にActive Recordで情報を保持する方法です。これによって、素早いレンダリングが可能になります。しかし、アソシエーションしているテーブルにある情報が膨大な場合、大量のメモリを消費することになります。

ActiveRecordのメソッドで言えば、`preload`, `eager_load`, `includes`などです。

#### Lazy loadingとは

Railsに限らず、Lazy loadingは遅延読み込みなどと言われたりしています。これは、JOINしたテーブルの情報が必要になった時に SQLを発行します。なので、メモリを確保する量は少なくてすみますが、JOINするテーブルを参照するたびSQLを発行するためWebサイト表示パフォーマンスを悪くする場合があります（N+1問題）。

ActiveRecordのメソッドで言えば、`joins`などです。

#### 補足

出てくる用語の理解が曖昧な場合、参照記事をぜひ読んください。

- ORM : [オブジェクト関係マッピング \- Qiita](https://qiita.com/yk-nakamura/items/acd071f16cda844579b9)
- SQLのJOIN : [SQL素人でも分かるテーブル結合\(inner joinとouter join\) \- Qiita](https://qiita.com/naoki_mochizuki/items/3fda1ad6594c11d7b43c)
- N+1問題 : [Rails「N\+1問題」超分かりやすい解説・解決方法【includesを使おう】 \| にょけんのボックス](https://nyoken.com/rails-includes)

## 主な違い

| メソッド      | SQL（クエリ）               | キャッシュ | アソシエーション先のデータ参照 | デメリット                          |
|------------|-------------------------|--------|-----------------------|----------------------------------|
| joins      | INNER JOIN              | しない   | できる                 | N + 1問題                         |
| preload    | JOIN せずそれぞれSELECT   | する     | できない               | IN句大きくなりがち                    |
| eager_load | LEFT JOIN               | する    | できる                  | LEFT JOINなので　相手が存在しなくても全部ロードしてしまう |
| includes   | 場合による                | する     | できる                 | ただしく理解してないと挙動がコントロールできない |

## 結論「どういう場合に使ったら良いか」

主な結論を一覧にしました。

- **`includes`はなるべく利用しない方が良い**（理由は[後述](https://qiita.com/ryosuketter/items/097556841ec8e1b2940f#%E6%A4%9C%E8%A8%BCpreload%E3%81%A8eager_load%E3%81%AE%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%AE%E9%81%95%E3%81%84)します）
    - 理由：意図しない挙動を防ぐため
    - 代わりに、`preload`か`eager_load`を使う

- `preload`
    - どんな場合に使うといいか ： **多対多のアソシエーションの場合**
    - できないこと ： **アソシエーション先のデータ参照（Whereによる絞り込みなど）**
    - 注意 ： データ量が大きいと、IN句が大きくなりがちで、メモリを圧迫する可能性がある

- `eager_load`はどんな場合に使うといいか
    - **1対1あるいはN対1のアソシエーションをJOINする場合**（belongs_to, has_one アソシエーション）
    - JOINした先のテーブルの情報を参照したい場合（Whereによる絞り込みなど）

- `joins`はどんな場合に使うか
    - メモリの使用量を必要最低限に抑えたい場合
    - JOINした先のデータを参照せず、絞り込み結果だけが必要な場合
        - 逆に言うと、引用先のデータを参照しない場合、使用しないほうがいいです

### 補足「preloadとeager_loadの使い分け」

データ量が膨大な場合だと、`preload`、`eager_load`は、いずれにせよ多くのメモリを必要とします。なので、自分はアソシエーションの種類によって使い分けるべきと考えます。

#### N対Nのアソシエーションの場合は`preload`

データ量が増えるほど、`eager_load`よりも、`preload`の方がSQLを分割して取得するため、レスポンスタイムは早くなるので、`preload`をオススメします。

#### 1対1あるいはN対1のアソシエーションの場合は`eager_load`

データ量が増えても、1回のSQLでまとめて取得した方が効率的な場合が多いと思うので、`eager_load`をオススメします。


## 作成するRailsアプリケーションの概要

具体例を示すために、実際にRailsで簡単なアプリケーションを作成しました。

作成するRailsアプリケーションは本や記事のレビューサイトを仮定しています。

- author : 著者
- article : 著書
- review : レビュー
- user : レビュー者

著者は自身が執筆した本や記事の情報を持てます。その本に対して、ユーザーはレビューを行えるサイトです。

### モデル

```rb:app/models/author.rb
class Author < ApplicationRecord
  has_many :articles
end
```

```rb:app/models/article.rb
class Article < ApplicationRecord
  belongs_to :author
  has_many :reviews
  has_many :users, through: :reviews
end
```

```rb:app/models/review.rb
class Review < ApplicationRecord
  belongs_to :article
  belongs_to :user
end
```

```rb:app/models/user.rb
class User < ApplicationRecord
  has_many :reviews
  has_many :articles, through: :reviews
end
```

### DB

![スクリーンショット 2019-12-29 10.52.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/f2e9e038-e855-108e-1336-a8289b35b813.png)

ER図の作り方は、下記の記事が参考になりました。

[MySQLWorkbenchでDBからER図\(モデル\)を作成 – リバースエンジニアリング \- 親バカエンジニアのナレッジ帳](https://ti-tomo-knowledge.hatenablog.com/entry/2018/05/21/082211)

## 環境

| OS | Ruby | Rails(ActiveRecord) |
|:-----------|------------:|:------------:|
| macOS High Sierra 10.13.6 | 2.6.5 | 5.2.4 |

## joins

どんな場合に使うと良いのかというと、`joins`の場合は、**JOINした先のデータを参照せず、絞り込み結果だけが必要な場面**かなと思います。
例えば、サイトで紹介している記事数を表示させたい場合、`joins`が良いでしょう。
具体例を作っていきます。

```rb:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @article_size = Author.joins(:articles).count
  end
end
```

```erb:app/views/articles/index.html.erb
<h1>Articles#index</h1>
<p>このサイトで紹介している記事数<%= @article_size %>点</p>
```

```sql:発行されたSQL
SELECT COUNT(*) FROM `authors` INNER JOIN `articles` ON `articles`.`author_id` = `authors`.`id`
```

## preload

どんな場合に使うと良いのかというと、`preload`の場合は、**N対Nのアソシエーションを取得する場面**かなと思います。
例えば、レビューがある本の、最初のレビューユーザー名とそのレビュー内容の一覧を表示したい場合、`preload`が良いでしょう。
具体例を作っていきます。

```rb:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.preload(:reviews, :users)
  end
end
```

```erb:app/views/articles/index.html.erb
<h1>Articles#index</h1>
<p>Title / Review サンプル(1) / サンプル Review ユーザ名(1)</p>
<ul>
  <% @articles.each do |article| %>
      <% if article.reviews.present? || article.users.present? %>
        <li>
          <%= article.title %> /
          <%= article.reviews.first.body %> /
          <%= article.users.first.username %>
        </li>
      <% end %>
  <% end %>
</ul>
```

```sql:発行されたSQL
Article Load (3.0ms)
SELECT `articles`.* FROM `articles`

Review Load (2.8ms)
SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`article_id` IN (1, ..省略.., 400)

User Load (0.5ms)
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (4, ..省略.., 85)
```

実際のWebサイト

![スクリーンショット 2019-12-31 12.23.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/07589f77-84ef-8774-a01d-a0d3a2a025aa.png)


## eager_load

どんな場合に使うと良いのかというと、`eager_load`の場合は、**JOINした先のデータを参照したい（絞り込み結果など）場面で**使います（アソシエーションも考慮したほいうがいいですが）。
例えば、本の中でもGOOD評価をしているレビューが何件あるかの一覧を表示したい場合、`eager_load`が良いでしょう。
具体例を作っていきます。

```rb:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article
      .eager_load(:reviews, :users)
      .where("reviews.body LIKE ?", "%GOOD%")
  end
end
```

```erb:app/views/articles/index.html.erb
<h1>Articles#index</h1>
<p>Title / User Name サンプル (1) / GOOD件数</p>
<ul>
  <% @articles.each do |article| %>
      <% if article.reviews.present? || article.users.present? %>
        <li>
          <%= article.title %> /
          <%= article.reviews.first.body %> /
          <%= article.users.size %>
        </li>
      <% end %>
  <% end %>
</ul>
```

```sql:発行されたSQL
SQL (6.0ms)  SELECT 
  `articles`.`id` AS t0_r0, 
  `articles`.`author_id` AS t0_r1, 
  `articles`.`title` AS t0_r2, 
  `articles`.`body` AS t0_r3, 
  `articles`.`created_at` AS t0_r4, 
  `articles`.`updated_at` AS t0_r5, 
  `reviews`.`id` AS t1_r0, 
  `reviews`.`article_id` AS t1_r1, 
  `reviews`.`user_id` AS t1_r2, 
  `reviews`.`body` AS t1_r3, 
  `reviews`.`created_at` AS t1_r4, 
  `reviews`.`updated_at` AS t1_r5, 
  `users`.`id` AS t2_r0, 
  `users`.`username` AS t2_r1, 
  `users`.`password_digest` AS t2_r2, 
  `users`.`email` AS t2_r3, 
  `users`.`dm` AS t2_r4, 
  `users`.`roles` AS t2_r5, 
  `users`.`reviews_count` AS t2_r6, 
  `users`.`created_at` AS t2_r7, 
  `users`.`updated_at` AS t2_r8 
FROM `articles` 
LEFT OUTER JOIN `reviews` ON `reviews`.`article_id` = `articles`.`id` 
LEFT OUTER JOIN `reviews` `reviews_articles_join` ON `reviews_articles_join`.`article_id` = `articles`.`id` 
LEFT OUTER JOIN `users` ON `users`.`id` = `reviews_articles_join`.`user_id` 
WHERE (reviews.body LIKE '%GOOD%')
```

実際のWebサイト

![スクリーンショット 2019-12-31 03.17.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/ba106a77-2338-69f8-d471-48d5e51411c8.png)

## 検証「preloadとeager_loadのレスポンスタイムの違い」

本記事の結論で述べた内容

「データ量が増えるほど、`eager_load`よりも、`preload`の方がレスポンスタイムは早くなる」

これを簡単に検証してみました。

### 検証方法

- 4回連続webブラウザから2回目から4回目までのリロードから得られたレスポンススピード平均値を出力
- レスポンスタイムは rails のログから得られる SQL のロードタイムから参照

### 検証データ

| Table          | data size | data size |
|----------------|-----------|-----------|
| data size type | 1         | 2         |
| articles       | 400       | 1000      |
| reviews        | 1000      | 1000      |
| users          | 30        | 190       |

### 検証結果

<img width="738" alt="スクリーンショット 2019-12-31 09.44.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/bb9f78d9-d18e-1eb6-c33a-8e251d71d74c.png">

検証による結論

- `eager_load`は、1回のSQLでJOINした全データを取得するので、データ量の増加に合わせてレスポンスタイムは長くなる
- `preload`も、データ量の増加に合わせてレスポンスタイムは長くなるが、`eager_load`ほどではないとグラフを見てわかる
    - （`preload`はJOINせず、SQLを分割して取得するので）
- なので、`preload`は`eager_load`よりも高速なレスポンスが期待できる
    - （ただし、本記事の結論で述べた、`preload`を使用した方がいい場面、できないことなどの条件に合致していれば）

補足

どの環境でも同等の結果になることは保証しない

## includes

本記事の結論で述べた通り、`includes`は利用しない方が良いでしょう。なぜなら、`includes`は、`preload`と`eager_load`をよしなに振り分けるので。`preload`と、`eager_load`の特徴を理解していれば、`includes`が登場する場面は、ほとんどないと思います。データが少ないうちは`includes`していても問題にならないかもしれませんが、データが増えてきたときにジワジワと問題が顕在化してくるので、`includes`の挙動も正しく知っておきましょう。リファクタの際に役立つと思います。

### 内部的にはどう使い分けているのか？

**アソシエーション先のデータ参照（WHEREなどを）しているかどうか**で使い分けています。

- 参照している場合 : `eager_load`
- 参照していない場合 : `preload`

具体例、下記の場合は、`eager_load`の挙動をします。

```rb:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:reviews, :users).where(reviews: { article_id: 1})
  end
end
```

具体例、下記の場合は、`preload`の挙動をします。

```rb:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:reviews, :users)
  end
end
```

どう振り分けているかの詳細は、下記の記事が参考になりました。

[ActiveRecordのincludes, preload, eager\_load の個人的な使い分け \| Money Forward Engineers' Blog](https://moneyforward.com/engineers_blog/2019/04/02/activerecord-includes-preload-eagerload/)

以上です。参考になれば幸いです。ありがとうございました。

## 参考

- [なぜ、SQLは重たくなるのか？──『SQLパフォーマンス詳解』の翻訳者が教える原因と対策 \- エンジニアHub｜若手Webエンジニアのキャリアを考える！](https://employment.en-japan.com/engineerhub/entry/2017/06/26/110000#%E5%8E%9F%E5%9B%A0ORM%E3%81%8C%E7%94%9F%E6%88%90%E3%81%99%E3%82%8BSQL%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%97%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84)
- [To join or not to join? An act of \#includes \- Goiabada](https://goiabada.blog/to-join-or-not-to-join-an-act-of-includes-f6728fcefea3)
- [Preload, Eagerload, Includes and Joins \| BigBinary Blog](https://blog.bigbinary.com/2013/07/01/preload-vs-eager-load-vs-joins-vs-includes.html)

[](グラフの生データ)
[](https://docs.google.com/spreadsheets/d/1JdDONDOJe5_kG85DVHxXtOc1lB5LMtc73-k5zFs66vc/edit?folder=0AAlFi5mijjjCUk9PVA#gid=0)

### その他

使用したseedファイル。

```rb:db/seeds.rb
1000.times do |i|
  Article.create!(
    author_id: rand(1..5),
    title: "Title #{i+1}",
    body: "Body #{i+1}",
  )
end

100.times do
  user_last_names = %W(佐藤 鈴木 高橋 田中 伊藤 渡辺 山本 中村 小林 加藤)
  user_first_names = %W(ハルト メイ ソウタ ヒマリ ミナト ハナ ユウト リン リク サクラ)
  names = []
  names << user_last_names.sample
  names << user_first_names.sample
  User.create!(username: names.join)
end

5000.times do
  first_review = %W(これは この本は この書籍は こいつは 本当にこれは)
  last_review = %W(やばい一冊だ とってもいい本だ つまらん BAD GOOD おもしろい！ つまらない 普通だ ビックリするくらい普通だ)
  reviews = []
  reviews << first_review.sample
  reviews << last_review.sample

  Review.create!(
    article_id: rand(1..1000),
    user_id: rand(1..100),
    body: reviews.join
  )
end
```