はじめに
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 |
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 |
class Item < ApplicationRecord
belongs_to :list
validates :name, presence: true
end
全リストを表示したい場合、controllerでallメソッドを使って取得し、view側でeachメソッドを用いて記述します。
class ListsController < ApplicationController
def index
@lists = List.all
end
(省略)
end
また、アソシエーションを組んでいるため、リストに紐づいた項目はlist.items
で取得可能です。
<% @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 %>
しかし、このコードを実行すると…
猫も走るほど忙しいこんな年末に…ターミナルも忙しいことになっています。
# @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テーブルへのアクセス1回
に対して、関連するテーブルがN回
発行されることから1+N
となり、N+1
と言われます。
N+1問題の対策
Railsでは
モデルに関連するデータを一度にまとめて取得することが出来ます。includes
メソッドを利用します。
引数にアソシエーションで定義した関連名を指定して定義します。
(テーブル名ではなく、関連名なのがポイントです)
Listモデルにhas_many :items
とアソシエーションを記述したので、コントローラーは下記のようになります。
class ListsController < ApplicationController
def index
@lists = List.all.includes(:items)
end
(省略)
end
その結果…忙しかったターミナルは
# @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を取得する際には
@items = Item.all.includes(:list)
と言うような記述になります。
これは子モデル側に、belongs_to :list
とアソシエーションを記述したからですね。
終わりに/感想
何となく「ターミナルにSQLがめっちゃある」位のイメージでしかなかったN+1問題でしたが、今回理解を深めることが出来たと感じます。
N+1問題を意識し、データベースを労わります。
こんなクリスマスも、たまには良いと思いました。
初学者で拙い記事ですが、少しでもお役に立てると嬉しく思います。
最後まで読んでいただき、誠にありがとうございました。