N+1問題を作りながら理解する(Rails初級者向け)

# N+1問題とは

ムダに`SQL`を発行させるせいでパフォーマンスが低下する問題です。

例えば、何かのリストを作成している時

- リストに表示するデータの取得に `SELECT` が1回実行される
- 各データの関連データを取得に`SELECT`が N回実行される
    - データベースアクセス（`SELECT`）が合計N+1実行されて、データ増えるほど不効率になる問題

普通にSQL書いてデータを取り出す場合、`JOIN`したりして1回のクエリで取得しようとしますが、Activerecordを使う際、意識しないと見落としがちなので、ORマッパーを使う場合に発生しがちと言えるでしょう。

もっと具体的なコードで見てみましょう。

# 作って見る

著者と、著者の書いた書籍のタイトルと書籍数を表示させたTOPページを作りたいとしましょう。

![スクリーンショット 2019-01-21 00.18.20.png](https://qiita-image-store.s3.amazonaws.com/0/14191/7279c10b-784c-e946-d256-f1d5c678dc3e.png)

## Viewの作成

ざっとviewの生成に必要な作業をしてみます。

コントローラ作成して

```bash
$ rails g controller top
```

## ルーティングの設定して

```rb:config/routes.rb
Rails.application.routes.draw do
  root 'top#index'
end
```

データもないので、タイトルだけ表示させましょう。

```erb:app/views/top/index.html.erb
<h1>TOP / index</h1>
```

コントローラの中身はまだ取り出すデータがないので、`index`アクションの中には何も書きません。

```rb:app/controllers/top_controller.rb
class TopController < ApplicationController
  def index

  end
end
```

Railsのローカル開発環境で作っていたら、`http://localhost:3000/`で、とりあえずタイトルだけは表示されると思います。

次に、著者と、著者の書いた書籍の生データを作っていきましょう。

## 下準備（データの作成）

著者と、著者の書いた書籍をモデルとしたい場合に、

Authorモデル（著者）と、Bookモデル（書籍）が、`1 : N`の関係になるようにしていきます。

`N+1`問題の理解用のカラム構成なので最低限のカラムだけでモデルを作成します。

```bash
$ rails g model author name:string
$ rails g model book author_id:integer name:string
```

railsコンソールで生データを作成していきましょう。

```bash:rails_console
# 著者のデータを挿入
Author.new(name:'Osamu Dazai').save
Author.new(name:'Kenji Miyazawa').save
Author.new(name:'Murakami Haruki').save
Author.new(name:'Hayao Miyazaki').save

# 著者(1)の本データを挿入
Book.new(author_id: 1, name: "Bannen" ).save
Book.new(author_id: 1, name: "Nijusseiki Kishu" ).save
Book.new(author_id: 1, name: "Hashire Merosu" ).save
Book.new(author_id: 1, name: "Ningen Shikkaku" ).save
Book.new(author_id: 1, name: "Good-Bye" ).save

# 著者(2)の本データを挿入
Book.new(author_id: 2, name: "Night on the Galactic Railroad" ).save
Book.new(author_id: 2, name: "Matasaburo of the Wind" ).save
Book.new(author_id: 2, name: "Gauche the Cellist " ).save

# 著者(3)の本データを挿入
Book.new(author_id: 3, name: "Norwegian Wood" ).save
Book.new(author_id: 3, name: "Sputnik Sweetheart" ).save
Book.new(author_id: 3, name: "Kafka on the Shore" ).save
Book.new(author_id: 3, name: "1Q84" ).save

# 著者(4)の本データを挿入
Book.new(author_id: 4, name: "My Neighbor Totoro" ).save
Book.new(author_id: 4, name: "Porco Rosso" ).save
Book.new(author_id: 4, name: "The Cat Returns" ).save
Book.new(author_id: 4, name: "Howl's Moving Castle" ).save
Book.new(author_id: 4, name: "Ponyo" ).save
Book.new(author_id: 4, name: "Arrietty" ).save
```

## データの関連付け

Railsコンソール上で、モデルになんのリレーションも書いてないと、下記のようにエラーになります。
booksというメソッドがないと指摘されていますね。なので関連付けしましょう。

```bash:rails_console
irb(main):027:0> Author.first.books
  Author Load (0.2ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):27
NoMethodError (undefined method `books' for #<Author:0x00007f89bc33eb20>)
```

モデルにリレーションを書くと...

```rb:app/models/author.rb
class Author < ApplicationRecord
  has_many :books
end
```

```rb:app/models/book.rb
class Book < ApplicationRecord
  belongs_to :author
end
```

このように、太宰治に関連づいた本を全て取得できる

```bash:rails_console
irb(main):030:0> Author.first.books
  Author Load (0.3ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."author_id" = ? LIMIT ?  [["author_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy 
[
#<Book id: 1, author_id: 1, name: "Bannen", created_at: "2019-01-20 06:12:36", updated_at: "2019-01-20 06:12:36">, 
#<Book id: 2, author_id: 1, name: "Nijusseiki Kishu", created_at: "2019-01-20 06:12:36", updated_at: "2019-01-20 06:12:36">, 
#<Book id: 3, author_id: 1, name: "Hashire Merosu", created_at: "2019-01-20 06:12:36", updated_at: "2019-01-20 06:12:36">, 
#<Book id: 4, author_id: 1, name: "Ningen Shikkaku", created_at: "2019-01-20 06:12:36", updated_at: "2019-01-20 06:12:36">, 
#<Book id: 5, author_id: 1, name: "Good-Bye", created_at: "2019-01-20 06:12:36", updated_at: "2019-01-20 06:12:36">
]>
```

## Viewに著者と書籍をリストで表示させるようにする

```rb:app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
    @authos = Author.all
  end
end
```

```erb:app/views/top/index.html.erb
<h1>TOP / index</h1>

<ul>
  <% @authos.each do |author| %>
  <li><%= author.name %>(<%= author.books.size %>)</li>
  <ul>
    <% author.books.each do |book| %>
      <li><%= book.name %></li>
    <% end %>
  </ul>
  <% end %>
</ul>
```

これで、ゴールとなる見た目は実装できますが、`N+1問題`が発生しています。

![スクリーンショット 2019-01-21 00.58.56.png](https://qiita-image-store.s3.amazonaws.com/0/14191/bf80352f-2da4-db93-0ac3-bdd35a98dd71.png)

わかりにくい図かもしれませんが、裏側では**9回**のSQLが発行されています。

## 発行されたSQLの内訳

- リストに表示するデータの取得に `SELECT "authors".* FROM "authors"` が1回実行される
- 各データの関連データを取得に
    - `SELECT "books".* FROM "books" WHERE "books"."author_id" = ?` が4回実行される
    - `SELECT COUNT(*) FROM "books" WHERE "books"."author_id" = ?` が4回実行される

データベースへのアクセスが合計`N+1`実行されて、新しい著者とその書籍のデータ増えるほどパフォーマンス上の不効率になる問題が起こっています。これが`N+1`問題です。

# 解決策

top_controllerにある、`index`アクションで生成させたインスタンス変数（`@authos`）には、`Author`モデルを`all`メソッドで全て取得させつつ、関連するテーブルをまとめて取得する`includes`メソッドです。
`includes`は、別のモデルへの参照情報を先読みしてメモリにキャッシュしておくことで、N+1問題を解決しています。

```rb:app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
    @authos = Author.all.includes(:books) # 30.0ms / 2sql
    # @authos = Author.all # 38.8ms / 9sql
  end
end
```

実際こう書くことで、裏側では発行されるSQLは2回で済んでいます。
コメントアウトで発行SQL数と、かかった時間を記載しています。時間にはブレがあります。

```sql
SELECT "authors".* FROM "authors"
SELECT "books".* FROM "books" WHERE "books"."author_id" IN (?, ?, ?, ?)
```

# まとめ

Activerecordでは、関連するモデルをもんだいなく効率的に取得する方法として`includes`などがあります。`includes`以外にも`join`などありますが、大事なことは、実発に発行されたSQLと、その処理にかかった時間を見て判断することなので、きちんと確認するのが良いと思います。

その際、`rack-mini-profiler`と言うパフォーマンス改善する時に使えるGemを使うと、SQLの発行回数や取得にかかった時間が分かるので、`N+1`問題が起こっていそうかの参考になると思います。

https://morizyun.github.io/ruby/library-rack-mini-profiler.html

# 参考

- https://railsguides.jp/association_basics.html
- http://www.techscore.com/blog/2012/12/25/rails%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E7%B4%B9%E4%BB%8B-n1%E5%95%8F%E9%A1%8C%E3%82%92%E6%A4%9C%E5%87%BA%E3%81%99%E3%82%8B%E3%80%8Cbullet%E3%80%8D/
- https://morizyun.github.io/ruby/library-rack-mini-profiler.html
- http://south37.hatenablog.com/entry/2014/08/10/Activerecord%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bincludes%E3%81%A8joins%E3%81%AE%E6%8C%AF%E3%82%8B%E8%88%9E%E3%81%84%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6