N+1問題とは
RailsのN+1問題とは、データベースから取得した1つのレコードに対して、関連するデータを取得するために、関連するテーブルに対して複数のSQLクエリを発行してしまう問題のことを指します。
例えばユーザー情報を取得する際に、そのユーザーが投稿した全ての記事の情報も取得する場合を考えてみましょう。
この場合、ユーザー情報を取得するための1つのSQLクエリを発行した後、全ての記事情報を取得するために、別途SQLクエリを発行する必要があります。
このため、ユーザーの数だけSQLクエリが発行されてしまい、処理時間が大幅に遅くなることがあります。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
@users.each do |user|
@post_count = user.posts.count
end
end
end
N+1問題は、Railsでよくある問題であり、性能劣化の原因となります。解決策として、Active Recordのincludes
メソッドやeager_load
メソッドを使用して、予め関連するデータを取得することが挙げられます。また、gemであるbullet
を使うことで、N+1問題を検出することもできます。
解消方法
このままではパフォーマンスが悪いアプリケーションのままです…
解消方法がいくつかあるので挙げていきます。
eager_loadを使用した場合
@users = User.eager_load(:posts)
LEFT_OUTER_JOINで指定したデータを結合して関連テーブルのデータを取得してきます。
引数として渡した関連先の要素で絞り込みを行うことが
preloadを使用した場合
@users = User.preload(:posts)
# 特定のタイトルというカラムのみを読み込みたい場合
@users = User.preload(posts: :title)
usersテーブルから必要なデータを一度に読み込み、さらにそれに紐付く投稿のタイトルのみを一度に読み込むことができます。
これにより、必要なデータだけを読み込むことができ、メモリ使用量を削減することができます。
includesメソッドを使用した場合
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.includes(:posts)
end
end
デフォルトではpreloadと同じ動きをし、関連先のテーブルの要素で絞り込みを行った場合などはeager_loadと同じ動きをします。
eager_load, preload, includesの使い分け
eager_load
has_one
、belongs_to
関連など1クエリでデータを取得した方が効率が良いと考えられる場合
preload
has_many
の関連を持つデータの事前読み込みを行う場合
includes
他テーブルを結合処理しているか、関連先のテーブルを絞り込みしている場合は挙動(eager_load
)が変わるため、リーダブルなコードを書くことを意識すると基本的に使うことは少なそう。
N+1問題の検出方法
解消方法は理解できたので、原因箇所の検出が必要です。
その際に使用できるのがbullet
というgemになります。
bullet
Bulletは、アプリケーションの動作中にN+1問題を検出し、警告を出力してくれます。
Gemfileへの追加
gem 'bullet', group: 'development'
generate コマンドで Bullet gem を有効にします
bundle exec rails g bullet:install
設定
明示的に指示を出す必要があるため、config/environments/development.rb
に下記コードを追加します。
設定は自身で変更可能です。Slackへの通知やN+1を検出した際にエラーを発生させることも可能です。
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true # Bullet gemの有効化
Bullet.alert = true # ブラウザ上でJavaScriptによるアラートをポップアップで表示
Bullet.bullet_logger = true # Bullet ログ ファイル (Rails.root/log/bullet.log) にログを記録
Bullet.console = true # ブラウザの console.log に警告を記録
Bullet.rails_logger = true # Rails ログに警告を直接追加
ブラウザにてコンソールログを確認
Bulletは、アプリケーションの動作中にN+1問題を検出し、コンソールログに警告を出力します。例えば、以下のようなログが出力されます。
N+1 Query detected
User => [:posts]
Add to your finder: :includes => [:posts]
ログに出力されたアドバイスに従い、includesメソッドを使ってN+1問題を解消することができます。
まとめ
N+1問題と向き合ってみました。
パフォーマンスの問題はアプリケーションが大きくなればなるほど向き合う数は増えていきます。
常日頃からパフォーマンスを低下させないリーダブルなコードを書けるよう意識していきます。