RailsでのN+1問題の原因と対策
N+1問題の原因
Lazy loading の設計で、テーブル情報が必要になった時だけ、SQLクエリを発行して取得する。
そのせいで、投稿一覧画面を表示するとき、個々の投稿の表示が処理されるたびに、ユーザー情報を取得するSQLクエリが発行されることになる。
n個の投稿があるなら、ユーザー情報を取得するクエリがn回発行される
SELECT "users".* FROM "users" WHERE "users"."id" = ?
x n回
ただ投稿コレクションを1回のクエリSELECT "boards".* FROM "boards"
で一括取得した
これで 1 + N の問題が起こっている。
対策法
Lazy loadingをeager loadingにする 。つまりテーブル情報を事前取得しておくこと。
Railsでは三つのメソッドを提供しているーー
1. includesメソッド利用
IN句で参照する情報を事前取得する -> SELECT "users".* FROM "users" WHERE "users"."id" IN (....)
ただwhere句で関係付けのテーブル情報を絞り込みたい場合は、referencesの指定が必要
Board.all.includes(:user).where('user.name = ?', '太郎').references(:users)
or
Board.all.includes(:user).where(users: {name:'太郎'})
2. preloadメソッド
includesの基本挙動と同じで、関係付けのテーブル情報の絞り込みが不可
3. eager_loadメソッド LEFT JOINを使って1つのクエリだけで関連するレコードをすべて取り出す。
includes.where.referencesの挙動と同じ
Rails 4以前は、includesがよしなにpreloadの方法か、eager_loadの方法か、振り分けてくれるけど、Rails 4以降は基本挙動はpreloadと同じになった。
N+1問題の検出方法
gem 'bullet'
N+1問題の発生を監視する
gem 'rack-mini-profiler'
SQLクエリ、CPUの消費時間などを計測する。N+1問題の解消前後のパフォーマンス変化が直感できる
参照:
Rails: JOINすべきかどうか、それが問題だ — #includesの振舞いを理解する
The (Silver) Bullet for the N+1 Problem
https://www.mehdi-khalili.com/orm-anti-patterns-part-3-lazy-loading