クリスマスのN + 1

# はじめに

12月25日。世間はクリスマス当日ですが、そんなことは置いておいて
分かっているようであやふやだった`N+1問題`について、備忘録として今一度まとめました。
(書いていたら、タッチの差で日付変わってましたが…)

## 開発環境
Ruby 2.6.5
Rails 6.0.3.4
MySQL
Visual Studio Code
(GoogleChrome)

# N+1問題とは
N+1問題とは、**テーブルがアソシエーションを利用することで、結びつくテーブルが多くなり、データベースへのアクセス回数が多くなってしまう問題**です。
SQLが沢山発行されてしまうことで、データベースに負荷がかかり、アプリケーションのパフォーマンス低下の原因となります。


## N+1問題の例
個人で作成した、ToDoリストアプリ的なもので例をあげます。
ToDoリストそのものはlistsテーブルと、リスト内の項目はitemsテーブルです。
一つのリストは沢山の項目を持っているので、下記のような1対多のアソシエーションが組まれています。

### listsテーブル
| Column | Type       | Options                        |
| ------ | ---------- | ------------------------------ |
| name   | string     | null: false                    |
| text   | text       |                                |
| trip   | references | null: false, foreign_key: true |


```ruby:lists.rb
class List < ApplicationRecord
  has_many :items
  validates :name, presence: true
end
```

### itemsテーブル
| Column    | Type       | Options                        |
| --------- | ---------- | ------------------------------ |
| name      | string     | null: false                    |
| text      | text       |                                |
| list      | references | null: false, foreign_key: true |

```ruby:item.rb
class Item < ApplicationRecord
  belongs_to :list
  validates :name, presence: true
end
```

全リストを表示したい場合、controllerでallメソッドを使って取得し、view側でeachメソッドを用いて記述します。

```rb:lists_controller.rb
class ListsController < ApplicationController
  def index
    @lists = List.all
  end
(省略)
end
```

また、アソシエーションを組んでいるため、リストに紐づいた項目は`list.items`で取得可能です。

```erb:index.html.erb
<% @lists.each do |list| %>　# 全てのリストを一覧で表示
　<div class="list">
　　<div class="list-name">
　　　リスト名：<%= list.name %>
　　</div>
　　<% list.items.each do |item| %>　# 各リスト内の項目を全て一覧で表示
　　　<div class="item-name">
　　　　<%= item.name %>
　　　</div>
　　<% end %>
　</div>
<% end %>
```
しかし、このコードを実行すると…
猫も走るほど忙しいこんな年末に…ターミナルも忙しいことになっています。

```console:ターミナル
# @List.allの挙動
  List Load (0.2ms)  SELECT `lists`.* FROM `lists`

# list.items.eachの挙動
  Item Load (0.2ms)  SELECT `items`.* FROM `items` WHERE `items`.`list_id` = 1
  Item Load (0.2ms)  SELECT `items`.* FROM `items` WHERE `items`.`list_id` = 2
  Item Load (0.1ms)  SELECT `items`.* FROM `items` WHERE `items`.`list_id` = 3
  Item Load (0.1ms)  SELECT `items`.* FROM `items` WHERE `items`.`list_id` = 4
  Item Load (0.1ms)  SELECT `items`.* FROM `items` WHERE `items`.`list_id` = 5
```
`list.items.each`によって、listに紐づいたitemsを探しに
**listsテーブルのレコード数だけ、itemsテーブルにアクセスしています。**
このように、必要以上にSQLを発行されてしまうのが`N+1問題`です。


**〜何故"N+1"と言うのか〜**
SQLがlistsテーブルへの1回アクセスする毎に、itemsテーブルへのアクセスがlistsテーブルのレコードの数だけ(上記では5回)発行されています。

このようにlistsテーブルへのアクセス`１回`に対して、関連するテーブルが`N回`発行されることから`1+N`となり、`N+1`と言われます。

# N+１問題の対策
## Railsでは
モデルに関連するデータを一度にまとめて取得することが出来ます。`includes`メソッドを利用します。
引数に**アソシエーションで定義した関連名**を指定して定義します。
(テーブル名ではなく、関連名なのがポイントです)

Listモデルに`has_many :items`とアソシエーションを記述したので、コントローラーは下記のようになります。

```rb:lists_controller.rb(修正)
class ListsController < ApplicationController
  def index
    @lists = List.all.includes(:items)
  end
(省略)
end
```

その結果…忙しかったターミナルは

```console:ターミナル
# @List.allの挙動
  List Load (0.2ms)  SELECT `lists`.* FROM `lists`

# list.items.eachの挙動
  Item Load (0.3ms)  SELECT `items`.* FROM `items` WHERE `items`.`list_id` IN (1, 2, 3, 4, 5)
```

itemsテーブルの情報も一度に取得するようになったので、`list.items.each`の挙動がスッキリしました！！


ちなみに、
> 引数に**アソシエーションで定義した関連名**を指定して定義します。
(テーブル名ではなく、関連名なのがポイントです)

と念を押したこの件ですが、1対多のアソシエーションの場合
多の方から1を取得する際には

```rb:(例)
@items = Item.all.includes(:list)
```
と言うような記述になります。
これは子モデル側に、`belongs_to :list`とアソシエーションを記述したからですね。



# 終わりに/感想
何となく「ターミナルにSQLがめっちゃある」位のイメージでしかなかったN+1問題でしたが、今回理解を深めることが出来たと感じます。
N+1問題を意識し、データベースを労わります。
こんなクリスマスも、たまには良いと思いました。

初学者で拙い記事ですが、少しでもお役に立てると嬉しく思います。
最後まで読んでいただき、誠にありがとうございました。



## 参考記事
[【Qiita】N+1問題](https://qiita.com/TsubasaTakagi/items/8c3f4317ad917924b860)
[【Pikawaka】Rails/N+1問題をincludesメソッドで解決しよう！](https://pikawaka.com/rails/includes)