N + 1問題, そしてincludesとは何なのか？

# はじめに 
Ruby on Railsを学習中ですが、その中でも、分かったような分からないようなモヤモヤする...
と個人的に感じた内容について、記事化していこうと思います。


### 今回のお題

#### 「N + 1問題とは何？, そして、includesを使うことのメリットは？」


「データベースからデータを取り出す際にincludesを使用すると、 N+1問題を解消し、効率的である(?)」

???:frowning2:
N+1問題や、それを解決するためのincludesメソッドについて、もやっとした理解だったので、
個人的に整理をつけるべく、まとめてみました。



# N + 1問題とは何だろう？

下記のようなアプリを考えます。

### チャットアプリ
よくある掲示板サイトのような感じです

- 機能
  - 複数人のユーザーがいる
  - チャットグループがある（スレッドのようなもの）
  - チャットグループには、チャットテーマがあり、複数人のメンバーが参加することができる
  - そのチャットグループに所属しているメンバーは、メッセージを投稿できる
![01_チャットアプリ概要.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/11c1cf05-d66e-94cb-36e5-74f9998385d9.png)


#### モデルとアソシーション

このチャットアプリは下記のmodelで成り立っています

- Model
  - Group
  - Message
  - User
  

**これらのmodelは、下記のような関係になっています**


- Group(チャットグループ)は、多数のMessagesを持っている
  - (MessageはGropに属している)
- Group(チャットグループ)には、多数のUser(メンバー)がいる
  -  (UserはGroupに属している)
- GroupとUserは多対多の関係である
  - Groupには多数のUserが存在し、また、Userも多数のGroupに属する
  - 中間テーブルが必要。 ※今回は説明は割愛させて頂きます

**ER図と、associationを示すコードは下記のようになります**

![スクリーンショット 2019-12-15 23.58.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/6be6d74c-cb08-618d-2777-76a7322d514c.png)

``` ruby
# group.rb
class Group < ApplicationRecord
  has_many :messages
  has_many :users, through: :group_users
  has_many :group_users
end

# message.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

# group_user.rb
class GroupUser < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

# user.rb
class User < ApplicationRecord
  has_many :messages
  has_many :groups, through: :group_users
  has_many :group_users
end
```

さて、以上の前提条件を元にして、
**メッセージ一覧を表示させる** 処理を行ってみましょう

下図のように、左のチャットグループ一覧から、閲覧したいグループをクリックすると、
右に、チャット概要（タイトルとメンバー）とメッセージ一覧が表示されます

![03_#index説明(修正済).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/da2cc07a-5071-18a4-00df-cc527ad2294a.png)



この処理を **indexメソッド** として、**messages_controller** に書いていきます



```ruby:message_controller.rb
  def index
    @group = Group.find(params[:group_id])
    @messages = @group.messages
  end
```
<解説>
1. チャットグループを選択した時に、`params[:group_id]`というparamsが送られるので、そのparamsから`@group = Group.find(params[:group_id])`で、対象のgroupを取得します
2. `@messages = @group.messages`にて、そのgroup内のmessagesを取り出しています


**取り出した`@messages`を表示させましょう！**
indexメソッドで取り出したデータを、ビュー`index.html.haml`に表示させます。

`@messages`内には4つの`message`が入っており、下図の赤枠のように表示させます。

![04_index.hamlの説明(修正).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/2aab1f01-9a21-cdc3-ac13-fe09cee658ce.png)


**赤枠部分**に対応するコードは下記になります
(※細かいクラス名などは省略しています)

```haml:index.html.haml
.messages
  - @messages.each do |message|
    = message.user.name
    = message.created_at.strftime("%Y/%m/%d(%a) %H:%M:%S")
    = message.content
```



このチャットグループには4件のmessageがあるので、**eachメソッド**で、`@messages`を1個ずつバラして`message`にして、その中のcontentや、関連するuser.nameといった情報を描画しています



ここで、この処理が行われる時に実行される、**SQLのクエリ**を見てみましょう
お馴染みの `rails s` でお世話になっている、ターミナルから確認することができます。



![07_N+1のSQL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/4d4ee20a-544c-3fa0-8d32-f0e083aceb70.png)

このSQLで、4個のメッセージを取得 => 描画するための必要なデータの取り出しが行われています。
上記の例だと、**計5個** のクエリが実行されています。4個のメッセージを表示させるのに、5回のクエリが実行される??:thinking:
これが、まさしく **N + 1 問題**の正体です。

ちなみに、このクエリと、先ほど実装したコードとの対応関係は下記のようになります。

<img width="1144" alt="07_SQLとコードの対応表(size修正済).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/35517c8b-4b05-21a2-eb01-f8971dd1c56b.png">


という流れになっています。
`@messages`を取得した後に、1件1件の`message.user.name`を取得するためにのクエリが**それぞれ** 実行されています。
今回は、メッセージが4個だけですが、これが何千・何万個とあれば、**その分+1個**のクエリが実行されてしまいます。これが、不特定多数の利用があったら、サーバーへの負荷は相当なものと考えられます。

これを解消するために、**includesメソッド** を使用しましょう :desktop:



# includesを使用してみる
#### includesを用いて、indexメソッドを書いてみましょう
先ほどの、messages_controllerの `@messages = @group.messages` に
**includesメソッド** を追記します。

```ruby:messages_controller.rb
  def index
    @group = Group.find(params[:group_id])
    @messages = @group.messages.includes(:user)
  end
```

簡単ですね。
さて、includesメソッドの引数 `:user`とは一体何でしょうか？

[railsガイド](https://railsguides.jp/active_record_querying.html)では、includesメソッドについて、下記のように説明されています。
>
Active Recordは、読み込まれるすべての関連付けを事前に指定することができます。
(中略)
includesを指定すると、Active Recordは指定されたすべての関連付けが最小限のクエリ回数で読み込まれるようにしてくれます。

上記のindexメソッドの場合、取得した`@group.messages`に関連する`user`の情報も、同時に取得出来るということです。

なんとなく分かったような、分からないような:hushed:
ここで、再度、SQLクエリを見てみましょう！

![08_includesを使用した場合のSQL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/37705c29-2376-c88c-98c6-fe578ad2ea85.png)


ずいぶんシンプルになりました！
先ほどは、**5回**実行されていましたが、includesメソッドを使うことで、**2回**に減りました。
これは、**includesメソッド**によって、`@group.messages`を取得する際に、関連する `user` の情報（メッセージを投稿したuserの情報）も取得しているので、たった2回で済むようになります。

#### 処理時間の比較

クエリの実行回数が減ることは確認出来ましたが、実際の処理時間に変化はあるのでしょうか:watch:
たった4個の情報の取得ですので、果たして差はあるのか？・・・
確かめてみました。

SQLクエリの実行時間は、下図のように、処理が終わった際に表示される、
`Completed 200 OK in 100ms (Views: 64.5ms | ActiveRecord: 2.3ms)`
のActiveRecordのミリ秒を評価しました(もし間違っていたらご指摘お願いします:beer:)

![09_SQL(includesなし) .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402722/d1357481-ce42-4fdc-2f03-501219595dfe.png)

- includesを使用しない場合
- includesを使用する場合

で、それぞれ **indexメソッド** を10回動作させてみて、ActiveRecordの動作時間の平均を取りました。

##### 結果
- includesを使用しない場合
  - **2.74ms**
- includesを使用する場合
  - **2.33ms**

劇的な変化はありませんでしたが、表示したいmessagesが大量の場合だと、かなり違ってくるのかもしれません。
(※興味がある方は、このようなアプリに、seedで大量にダミーデータを入れてみて、検証してみるのもいいかもしれません)

# おわりに
RubyやRailsなどを学習してきて、なんとなく分かったような、分からないような概念やメソッドなど結構あるのですが、今回、そのようなテーマを深堀りし、個人的に **モヤモヤからスッキリ** に変わったような気がします。
includesメソッドが無くても機能的には何ら問題はないのですが、サーバーの内部的には負荷が違ってくるし、業務用のアプリで膨大な件数のデータを取得する時は、目に見える形で効率が違うのかもしれません。