eager_load/preload/includes/joinsの違いメモ

# 動機
日頃Goしか書いてないGopherがRails書いたときにつまったActiveRecord、特にjoins周りの挙動が直感的によくわからなかったので実際にデータ用意して調べてみた
(メソッドの具体的な説明は他にたくさん記事があるのでそちらを参照してください。)

# 実行環境
[Rails公式Docのサンプルブログアプリ](
https://railsguides.jp/getting_started.html#%E3%83%96%E3%83%AD%E3%82%B0%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)
の `article.rb` と `comment.rb` のモデルを使用。
今回は、それぞれ約25万行を用意した。

```article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  validates :title, presence: true,
                    length: { minimum: 5 }
end
```

```comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end
```

Controllerから `preload/eager_load/joins/includes` を用い、それぞれの吐くSQLやパフォーマンスを比較してみる

# 0. 素
`Article.limit(100)`

```sql
SELECT `articles`.* FROM `articles` LIMIT 100
-- Completed 200 OK in 154ms (Views: 127.1ms | ActiveRecord: 15.7ms | Allocations: 20418)
```
まずは検証用にそのままで...

# 1. eager_load
`Article.eager_load(:comments).limit(100)`

```sql
SQL (1.9ms)  SELECT DISTINCT `articles`.`id` FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
SQL (575.4ms)  SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`text` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`commenter` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`article_id` AS t1_r3, `comments`.`created_at` AS t1_r4, `comments`.`updated_at` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4869ms (Views: 4280.1ms | ActiveRecord: 585.9ms | Allocations: 2103879)
```
万能感ある。駆動表のキャッシュをしながら、絞り込みもできる。IN長すぎワロタ。

# 2. preload
`Article.preload(:comments).limit(100)`

```sql
Article Load (1.9ms)    SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (405.8ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4304ms (Views: 3883.6ms | ActiveRecord: 416.9ms | Allocations: 1658640)
```
eager_loadより省エネな感じ。ただ、最後のSELECT文がcommentsテーブルしか参照してないので絞り込みができない

# 3. includes
`Article.includes(:comments).limit(100)`

```sql

Article Load (1.5ms)  SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (384.1ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 3737ms (Views: 3339.1ms | ActiveRecord: 395.3ms | Allocations: 1652329)
```
今回はpreloadと同じ挙動
（includes は場合によって挙動が変わる. 最後のSELECT文にないテーブルのwhereとかはできないので、そういう時はeager_loadになる）

```ruby
def eager_loading?
  @should_eager_load ||=
    eager_load_values.any? ||
      includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end
```


# 4. joins
`Article.joins(:comments).limit(100)`

```sql
Article Load (2.1ms)  SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
-- Completed 200 OK in 67ms (Views: 56.1ms | ActiveRecord: 7.0ms | Allocations: 21703)
```
いつものアレ。安心感ある。

# まとめ
そもそもActiveRecordは LazyLoad、すなわち **必要になったときにSQLが実行される** 。
for文の中でSQLが複数回発行(N+1)されないために、こういう機構が必要になってくる。
（生SQLerの私にはこの感覚があんまりない）

後は、where使いたいなら `eager_load` 何もしないなら `preload` って感じ。
頭使いたくない人は 「とりあえず `includes`」 しとけばなんとかなる。
ただ、複数紐づくテーブル扱い出すと最終的に吐き出されるSQLが予測困難になるので、ちゃんと使い分けたいところ。
ちゃんと使い分けたい人は[この記事](https://moneyforward.com/engineers_blog/2019/04/02/activerecord-includes-preload-eagerload/)とか読みましょう。実装追ってて勉強になります。