N+1問題とは
N+1問題とは、大量のSQLが実行されて動作が重くなるという問題。
「全てのレコードを取得する1回+N回分SQLが実行される」ために、N+1問題という。
ex.)掲示板一覧ページにアクセスした時
- 掲示板(Board)のデータををallで取得(1回)
- その各掲示板を投稿したユーザーの名前を表示させたい。
→Usersテーブルからデータを取得する必要がある。
→掲示板の数の分だけusersテーブルから名前を取得するSQLを実行する。
→掲示板の数が10個あれば、10回SQLを実行する。
仮にこれが1万回もSQLを実行した場合、めちゃくちゃ重くなる。
以下のログでは、掲示板を全て取得してから、ユーザーの情報を取得するためのSQLが何度も実行されている。
Started GET "/boards" for ::1 at 2020-03-20 16:52:47 +0900
Processing by BoardsController#index as HTML
Rendering boards/index.html.erb within layouts/application
Board Load (1.2ms) SELECT "boards".* FROM "boards"
↳ app/views/boards/index.html.erb:17
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/views/boards/index.html.erb:34
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/views/boards/index.html.erb:34
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/views/boards/index.html.erb:34
N+1の解決策
・includes
メソッドで関連するモデルのデータを先に取得しておく。
@boards = Board.all.includes(:user)
Started GET "/boards" for ::1 at 2020-03-20 17:19:28 +0900
Processing by BoardsController#index as HTML
Rendering boards/index.html.erb within layouts/application
Board Load (0.3ms) SELECT "boards".* FROM "boards"
↳ app/views/boards/index.html.erb:17
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8], ["id", 9], ["id", 10]]
↳ app/views/boards/index.html.erb:17
Rendered boards/index.html.erb within layouts/application (51.0ms)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 10], ["LIMIT", 1]] (※ログイン中のユーザー)
↳ app/views/layouts/application.html.erb:13
Rendered shared/_header.html.erb (2.5ms)
Rendered shared/_flash_message.html.erb (0.4ms)
Rendered shared/_footer.html.erb (0.4ms)
Completed 200 OK in 106ms (Views: 102.6ms | ActiveRecord: 0.9ms)
これで、仮にeachメソッドで繰り返し表示させた場合でも、毎回SQLを発行せずに済む。
bulletジェムで、N+1問題を検出
bulletジェムをインストールしておけば、ブラウザ上でn+1問題が発生しているビューを表示したときに、ブラウザにポップアップで警告を出してくれる。
開発環境にインストールしておけばよい。
Gemfile
group :development do
gem 'bullet'
end
ちなみにターミナルのログにも警告を表示してくれて、includesメソッドを使ってくださいね〜って勧めてくれる。
親切だこと。
user: funesakisuke
GET /users/124/bookmarks
USE eager loading detected
Board => [:user]
Add to your query: .includes([:user])
Call stack