LoginSignup
8
3

More than 1 year has passed since last update.

RailsでのN+1問題と向き会ってみる

Posted at

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_onebelongs_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問題と向き合ってみました。
パフォーマンスの問題はアプリケーションが大きくなればなるほど向き合う数は増えていきます。
常日頃からパフォーマンスを低下させないリーダブルなコードを書けるよう意識していきます。

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3