【個人メモ】N+1問題 preload、eager_loadのまとめ

# N+1の対策メソッド
1. joins
1. left_outer_joins
1. eager_load
1. preload
1. includes

eager_load、preload、includesは、一度取得したデータをキャッシュする。2回目以降は、DBではなく、メモリ上にキャッシされたデータを取得することになる。
キャッシュが不要な場合は、joins或いはleft_outer_joinsを使用する。これらはSQLの句なので、この記事で詳細は触れない。

# N+1問題とは？
ループ処理の中で都度SQLを発行してしまう問題のこと。
[【Ruby on Rails】N+1問題ってなんだ？](https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361)

# preload
joinは行われず、2つのテーブに対してそれぞれSQLを発行する。（=2回SQLを発行）
joinを行わないため、where句による絞り込みが出来ない。
レコード量が多い場合、joinの処理を行うeager_loadはパフォーマンスや負荷に影響する場合がある。
where句による絞り込みが不要な場合は、preloadを優先的に使用する。

```erb
 <% @articles.each do |article| %>
   <% article.tags.each do |tag| %>
     <%= tag.name %>
   <% end %>
 <% end %>
```
```ruby
 def index
   @user = User.find(1)
   - @articles = @user.articles #修正前
   + @articles = @user.articles.preload(:tags) #修正後
 end
```
修正前
```sql
  Tag Load (0.1ms)  SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ?  [["article_id", 698]]
  ↳ app/views/profiles/_articles.html.erb:17
  Tag Load (0.1ms)  SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ?  [["article_id", 699]]
  ↳ app/views/profiles/_articles.html.erb:17
  Tag Load (0.1ms)  SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ?  [["article_id", 700]]
```
修正後
```sql
 Tag Load (8.9ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (1718, 3985, 937, 1418, 4902, 4760, 2461, 641, 2236, 4994, 4474, 4190, ・・・
```
# eager_load
ネストした関連モデルのデータを一括取得する場合にも使用する。ネストした関連テーブとは、1対多対多（或いは多対多対多など）のような関連付けのテーブルのこと。
下記は単に関連テーブルの取得。
```erb
 <% @skill_categories.each do |category| %>
    <% category.skills.each do |skill| %>
      <%= skill.name %>
    <% end %>
 <% end %>
```
```ruby
 def user_reccomend_skill_categories
  - @user.skills.map(&:skill_category).
      filter { |skill_category| skill_category.reccomend }.uniq

  + SkillCategory.eager_load(:skills).
      where(reccomend: true).
      where(skills: { user_id: @user.id })
 end
```
```sql
 Skill Load (0.1ms)  SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ?  [["skill_category_id", 929]]
  ↳ app/views/profiles/_user.html.erb:42
  Skill Load (0.1ms)  SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ?  [["skill_category_id", 896]]
  ↳ app/views/profiles/_user.html.erb:42
  Skill Load (0.1ms)  SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ?  [["skill_category_id", 1265]]
  ↳ app/views/profiles/_user.html.erb:42
```
```sql
 SQL (7.4ms)  SELECT "skill_categories"."id" AS t0_r0, "skill_categories"."name" AS t0_r1, "skill_categories"."reccomend" AS t0_r2, "skill_categories"."created_at" AS t0_r3, "skill_categories"."updated_at" AS t0_r4, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."user_id" AS t1_r2, "skills"."skill_category_id" AS t1_r3, "skills"."created_at" AS t1_r4, "skills"."updated_at" AS t1_r5 FROM "skill_categories" LEFT OUTER JOIN "skills" ON "skills"."skill_category_id" = "skill_categories"."id" WHERE "skill_categories"."reccomend" = ? AND "skills"."user_id" = ?  [["reccomend", 1], ["user_id", 1]]
```

# includes
状況に応じて、よしなにpreload、eager_loadで振る舞う。どちらを意図しているのか、コード上で分かりづらい為非推奨。