associationをキャッシュした場合の速度

## はじめに
N+1はパフォーマンスに影響することは知っていると思います。
ただ、パフォーマンスに影響するということはわかっても具体的にどれくらいの差があるのかを数値で見る機会は、なかなかったので、キャッシュしないものとキャッシュするものを比較してみたいと思います。

## テーブル関係
親usersテーブル、子postsテーブルを用意します。
```rb:app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end
```
```rb:app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end
```

## seedの用意
以下のようなseedデータを用意します。データの件数は1000件です。
```rb:db/seeds.rb
1000.times do |n|
  user = User.create!(
    name: "山田 太郎",
    email: "sample#{n + 1}@sample.com"
  )

  Post.create!(
    user_id: user.id,
    title: "タイトル#{ n + 1 }",
    body: "これは～のであり、～である。"
  )
end
```

## view側
usersテーブルに紐づくpostsテーブルの要素を表示しています。
```erb
<% @users.each do |user| %>
  <% user.posts.each do |post| %>
    <%= post.title %>
  <% end %>
<% end %>
```

## joinsの速度
以下のコードを実行します。
```rb
@users = User.joins(:posts)
```
内部結合はassociationをキャッシュをしません。なので`ActiveRecord: 329.3ms`の時間がかかっています。

```sql
User Load (2.4ms)  SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id`

Post Load (0.9ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1
Post Load (0.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 2
Post Load (1.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 3
Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 4
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 5
Post Load (0.2ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 6
---
Rendered users/test.html.erb within layouts/application (Duration: 1261.9ms | Allocations: 526914)
Rendered layout layouts/application.html.erb (Duration: 1264.0ms | Allocations: 529231)
Completed 200 OK in 1346ms (Views: 937.9ms | ActiveRecord: 329.3ms | Allocations: 588057)
```

## left_joinsの速度
以下のコードを実行します。
```rb
@users = User.left_joins(:posts)
```
左外部結合もassociationをキャッシュしません。なので、joinsと同じような速度`ActiveRecord: 297.3ms`になります。
```sql
User Load (1.6ms)  SELECT `users`.* FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id`

Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1
Post Load (0.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 2
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 3
Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 4
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 5
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 6
---
Rendered users/test.html.erb within layouts/application (Duration: 1127.0ms | Allocations: 531034)
Rendered layout layouts/application.html.erb (Duration: 1141.5ms | Allocations: 556218)
Completed 200 OK in 1210ms (Views: 853.5ms | ActiveRecord: 297.3ms | Allocations: 620498)
```

## includesの速度
以下のコードを実行します。
```rb
@users = User.includes(:posts)
```
includesはassociationをキャッシュします。速度としては`ActiveRecord: 4.9ms`となりました。キャッシュしていないものに比べて約1/60の速度となっています。
```sql
User Load (0.9ms)  SELECT `users`.* FROM `users`
Post Load (2.7ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, 4, 5, 6,~,1000)

Rendered users/test.html.erb within layouts/application (Duration: 8.3ms | Allocations: 17176)
Rendered layout layouts/application.html.erb (Duration: 19.6ms | Allocations: 41066)
Completed 200 OK in 103ms (Views: 20.9ms | ActiveRecord: 4.9ms | Allocations: 140693)
```

## eager_loadの速度
以下のコードを実行します。
```rb
@users = User.eager_load(:posts)
```
eager_loadはassociationをキャッシュします。速度としては`ActiveRecord: 5.2ms`となりました。キャッシュしていないものに比べてこちらも約1/60の速度となっています。
eager_loadはLEFT OUTER JOINを使用するので、SQLクエリが一つになります。また、eager_loadの場合は`eager_load(:posts).where(posts: { id: 1 })`のような絞り込みをすることができます。大量のデータを持たないテーブルで絞り込みたい場合はこちらを使うのがよさそうです。
```sql
SQL (3.0ms)  SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, 
`users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `posts`.`id` AS t1_r0, 
`posts`.`title` AS t1_r1, `posts`.`body` AS t1_r2, `posts`.`user_id` AS t1_r3, 
`posts`.`created_at` AS t1_r4, `posts`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN 
`posts` ON `posts`.`user_id` = `users`.`id`

Rendered users/test.html.erb within layouts/application (Duration: 5.8ms | Allocations: 17194)
Rendered layout layouts/application.html.erb (Duration: 14.5ms | Allocations: 41078)
Completed 200 OK in 81ms (Views: 15.8ms | ActiveRecord: 5.2ms | Allocations: 143458)
```

## preloadの速度
以下のコードを実行します。
```rb
@users = User.preload(:posts)
```
preloadはassociationをキャッシュします。速度としては`ActiveRecord: 5.8ms`となります。キャッシュしていないものに比べてこちらも約1/60の速度となっています。
preloadはjoinをしないので、大量のデータを持つテーブルや、複雑なクエリの場合はこちらを使うのが適しているでしょう。ただ、eager_loadのような子テーブルの絞り込みはpreloadではできません。
```sql
User Load (0.9ms)  SELECT `users`.* FROM `users`
Post Load (1.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, 4, 5, 6,~,1000)

Rendered users/test.html.erb within layouts/application (Duration: 6.6ms | Allocations: 17180)
Rendered layout layouts/application.html.erb (Duration: 17.4ms | Allocations: 41355)
Completed 200 OK in 95ms (Views: 18.5ms | ActiveRecord: 5.8ms | Allocations: 141373)
```

## まとめ
| メソッド          | クエリ数 | JOINの使用 | 絞り込み | ActiveRecord時間 | 大量のデータを扱うのに適しているか | アソシエーション                                      |
|-------------------|----------|------------|----------|-----------------|---------------------------------|---------------------------------------------------|
| `joins`           | N + 1        | はい         | 可能       | 約329.3ms        | いいえ                           | キャッシュしない              |
| `left_joins`      | N + 1        | はい         | 可能       | 約297.3ms        | いいえ                           | キャッシュしない              |
| `includes`        | 1または2        | 場合による       | 場合による     | 約4.9ms          | 場合による                             | キャッシュする               |
| `eager_load`      | 1        | はい         | 可能       | 約5.2ms          | いいえ                           | キャッシュする              |
| `preload`         | 2        | いいえ       | いいえ     | 約5.8ms          | はい                             | キャッシュする               |

includesは場合によって、eager_loadの挙動かpreloadの挙動を行うようです。
ただ、意図した挙動を行いたい場合は、eager_load、preloadを指定するのが良いかと思います。


## 参考

https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58