preload、eager_load、includesの違いをいつも忘れてしまうのでメモしておく

## 概要

忘れん坊な自分のために、preload、eager_load、includesの違いをメモしておきます😮‍💨
## これらのメソッドは何のためにあるのか
結論、N+1問題が発生しないようにするため。

特定のレコードを取得する際、これらのメソッドを使って関連レコードを先読みして取得しておくことでN+1が発生しません。

## N+1のおさらい
 N+1とは **データベースへのアクセス回数が余計に多くなってしまう現象** です。

例えば全てのユーザーの名前と、関連する最初の投稿（posts）のタイトルを出力するコードがあるとします。
```ruby
users = User.all

users.each do |user|
  puts "#{user.name}の最初の投稿は#{user.posts.first}です。"
end
```
上記を実行した時のSQLは以下になります。
```sql
SELECT "users".* FROM "users"

SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 2 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 3 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 4 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 5 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 6 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 7 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 8 ORDER BY "posts"."id" ASC LIMIT 1
```
eachの内部で`user.posts.first`が呼び出される度に、postsを取得する余分なSELECT文が発行されており、N+1が発生しています。

少ないレコード件数であればそこまで問題にはならないですが、これが10万、100万件の場合はパフォーマンスに大きな影響が出てしまいます。

## includes、preload、eager_loadでN+1は解消できる

しかし、以下のようにincludes（preload、eager_loadでも同じ）を呼び出した上でeachすれば
```ruby
users = User.includes(:posts)

users.each do |user|
  puts "#{user.name}の最初の投稿は#{user.posts.first}です。"
end
```

users取得時に関連するpostsを一括取得するSQLが発行されるので、N+1が起きません。
```sql
SELECT "users".* FROM "users"

SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (1, 2, 3, 4...)
```

## ではそれぞれの違いはなに？
関連レコードを一緒に読み込むという点では一緒ですが、発行されるSQLに違いがあります。

## preload
関連レコードを別のクエリ（SELECT）で取得します。
```ruby
users = User.preload(:posts)
```
```sql
SELECT "users".* FROM "users"

SELECT "posts".* FROM "posts" WHERE "user_id" IN (1, 2, 3, 4...)
```
### 特徴
- データ量が大きいものを扱う場合にeager_load（`left join`を）よりも早く取得できる
    - ただしレコード件数が非常に大きい場合、IN句に指定される数も膨大になるため、以下のようなSQLのメモリ上限に気を付ける
        - SQLのサイズが[max_allowed_packet](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet)の設定値を超えた場合にエラーとなる
        - [range_optimizer_max_mem_size](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_range_optimizer_max_mem_size)に設定された範囲検索のメモリ上限をオーバーしてしまい、インデックスが使われない
- 関連レコードを複数指定した場合、その数だけ発行されるクエリが増える
- whereなどで関連レコードの絞り込みができない（絞り込もうとするとエラーになる）

## eager_load
関連レコードを`LEFT OUTER JOIN`を使って一つのクエリで一括取得します。

```ruby
users = User.eager_load(:posts)
```

```sql
SELECT "users".* FROM "users"
LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
```
### 特徴
- whereなどでレコードの絞り込みができる
- 関連レコードを複数指定した場合でも一つのクエリで済む
- `left join`でテーブル同士を結合するため、データ量が大きいものを扱う場合にスロークエリとなる可能性がある


> left joinでテーブル同士を結合するため、データ量が大きいものを扱う場合にスロークエリとなる可能性がある

こちらですが、特に**has_manyアソシエーションかつデータ量が大きい場合**はスロークエリが発生しやすいので注意が必要です。

```ruby
User.eager_load(:has_many_association).limit(10)
```

たとえば、上記のようにhas_manyの関連先があるUserを`limit(10)`で絞り込む場合、eager_loadを使うと1対N関連のテーブルを`left join`で取得するため、重複を含んでいた場合は10件以上のレコードが取得されてしまいます。

なので、ActiveRecord内部では「has_manyをeager_loadしているかつ、`limit` or `offset`で絞り込みしている場合」に`distinct`付きのSQLが発行されるようになっている（[参考](https://moneyforward-dev.jp/entry/2019/04/02/activerecord-includes-preload-eagerload/#:~:text=has_many%E3%82%A2%E3%82%BD%E3%82%B7%E3%82%A8%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92eager_load%E3%81%99%E3%82%8B%E3%81%A8%E3%82%B9%E3%83%AD%E3%83%BC%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%97%E3%82%84%E3%81%99%E3%81%84)）のですが、データ量が大きいと、この`distinct`が要因でスロークエリとなりやすいわけです。


## includes
Railsが自動的にpreload、eager_loadを使い分けてくれます。

includesのみを使った場合は、preloadが内部的に使われます。
```ruby
users = User.includes(:posts)
```
```sql
SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts" WHERE "user_id" IN (1, 2, 3, 4...)
```
includesと絞り込みのクエリ（whereやlimit, offsetなど）を使った場合は、eager_loadが内部的に使われます。
```ruby
users = User.includes(:posts).where(posts: {title: 'preload、eager_load、includesの違いをいつも忘れてしまうのでメモしておく'})
```
```sql
SELECT "users".* FROM "users" 
LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" 
WHERE "posts"."title" = 'test'
```
### 特徴
- 絞り込みが使える/使えないを意識しなくてもrailsがよしなに変換してくれるので楽
- 使い方によってクエリが変わるので、何も考えずに使うとスロークエリになる可能性がある

## それぞれどう使い分けるのが良いか
使い方によってクエリが変わっても良いという場合や、パフォーマンスを気にしないのであれば、深く考えずにincludesを使うで良いと思います。


しかしクエリを制御したいという場合や、パフォーマンスを気にするのであれば、発行されるSQLを意識した上でeager_loadとpreloadを使い分けた方が良いです。

<br>

個人的な目安としては以下のように考えています。


**preloadを使う場合**
- 関連レコードで絞り込みの必要がない
- データ量が大きいものを扱う
- 取得する関連先が多い
    - `eager_load`だと`left join`で関連先が多ければ多いほど重くなる可能性がある

**eager_loadを使う場合**
- 関連レコードで絞り込みが必要
- データ量がそこまで大きくないものを扱う
- has_manyでない（has_one）テーブルで絞り込みを行うとき

## 終わりに

これで違いを忘れることがなさそうです😌

またパフォーマンス向上のためには、Active Recordのメソッドを使う際にどのようなSQLが発行されるかを意識していくことが大事だと改めて感じました。

## 参考にさせていただいた記事
https://zenn.dev/goldsaya/articles/3af48dadc6cc0f

https://zenn.dev/iloveomelette/articles/6be524dd7628b6

https://moneyforward-dev.jp/entry/2019/04/02/activerecord-includes-preload-eagerload/