N+1問題とは
一覧ページの表示等で、1件表示に対して1つのクエリを発行してしまう事をN+1問題という。
N+1が起きるとDBとの通信回数が多くなりレスポンスが遅くなるのでパフォーマンスが落ちる。
特にRailsのActiveRecordなどのORMを使っている初学者は、生身のクエリを意識せずに実装できてしまうため、この問題に気付きにくいと思う。
どうやって見つける?
N+1の発生箇所を検出してくれるbulletという便利なgemがある。
bulletは、N+1を検出するとウインドウ表示してくれるので書かれている対象を修正していけば良い。
導入方法
bulletのインストール
development環境に入れるので developmentグループに以下を追加します
test-app/Gemfile
gem 'bullet'
bulletのconfig設定
config/environments/development.rb
# Bullet gem config
test-app::Application.configure do
config.after_initialize do
Bullet.enable = true # Bulletプラグインを有効
Bullet.alert = true # JavaScriptでの通知
Bullet.bullet_logger = true # log/bullet.logへの出力
Bullet.console = true # ブラウザのコンソールログに記録
Bullet.rails_logger = true # Railsログに出力
end
end
bulletを使う
N+1の検出
develop環境でrails serverを起動し、特に動作の遅いページを表示する。
bulletがN+1を自動検出してくれるのでウインドウ表示を確認する。
N+1の対策
ActiveRecordのクエリにincludesを記述する。
(includesで無駄なクエリを一つに束ねることができる)
例)ransackに発生していたN+1(修正後)
def set_ransack
@q = User.sort_by_name.ransack(params[:q])
@users = @q.result(distinct: true).page(params[:page]).includes(image_attachment:[:blob])
end
クエリのパフォーマンス測定
N+1の改善後、どの程度パフォーマンスに変化があるか測定しよう。
(改善前・改善後で実行値を比較する)
EXPLAINコマンドで、N+1が発生していたクエリの実行計画を測定する
$ EXPLAIN SELECT * FROM item;
QUERY PLAN
---------------------------------------------------------
Seq Scan on item (cost=0.00..12.20 rows=220 width=328)