Skip to content

N+1問題はこれで解決!Bullet gemの導入と実践的な使い方

はじめに

Railsアプリケーションのパフォーマンスを低下させる最も一般的な原因の一つが「N+1クエリ問題」です。開発中は気づきにくいものの、データ量が増えるにつれて顕在化し、ページの表示速度を著しく悪化させます。幸いなことに、Railsコミュニティにはこの問題を検出し、解決策を提示してくれるBulletという強力なgemが存在します。

この記事では、N+1問題とは何か、そしてBullet gemを使ってそれをどのように特定し、解決していくかを具体的なコード例と共に解説します。

N+1クエリ問題とは?

N+1クエリ問題とは、1つのクエリで親のレコードを取得し、その後、各親レコードに関連する子のレコードを取得するために、親の数(N)だけ追加のクエリが発行されてしまう状況を指します。

例えば、ブログの記事一覧ページで、各記事の投稿者名も表示したい場合を考えてみましょう。

問題のあるコード

コントローラ

ruby
# app/controllers/articles_controller.rb
def index
  @articles = Article.all
end

ビュー

erb
<%# app/views/articles/index.html.erb %>
<% @articles.each do |article| %>
  <div>
    <h2><%= article.title %></h2>
    <p>投稿者: <%= article.user.name %></p> <%# ここでN+1が発生 %>
  </div>
<% end %>

このコードが実行されると、Railsのログには以下のようなSQLクエリが記録されます。

sql
-- 1. まず、すべての記事を取得する (1回)
SELECT "articles".* FROM "articles"

-- 2. その後、各記事のユーザー情報を取得するために、記事の数だけクエリが発行される (N回)
SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
SELECT "users".* FROM "users" WHERE "users"."id" = 2 LIMIT 1
SELECT "users".* FROM "users" WHERE "users"."id" = 3 LIMIT 1
-- (記事の数だけ続く...)

もし記事が100件あれば、1 + 100 = 101回のクエリがデータベースに発行されてしまいます。これがN+1問題です。データが少ない開発環境では問題になりませんが、本番環境では深刻なパフォーマンスボトルネックとなります。

Bulletの導入と設定

Bulletは、このようなN+1クエリを開発中に検出し、開発者に警告してくれるgemです。

1. インストール

GemfileにBulletを追加します。開発環境でのみ使用するため、:developmentグループに記述します。

ruby
# Gemfile
group :development do
  gem 'bullet'
end

そしてbundle installを実行します。

bash
bundle install

2. 設定

次に、config/environments/development.rbにBulletの設定を追記します。これにより、Bulletが有効になり、検出した問題をどのように通知するかを指定できます。

ruby
# config/environments/development.rb
Rails.application.configure do
  # ... (既存の設定)

  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = true # ブラウザにJavaScriptのアラートを表示
    Bullet.bullet_logger = true # log/bullet.log にログを出力
    Bullet.console = true # ブラウザのコンソールにログを出力
    # Bullet.growl = true # Growl通知 (Mac)
    # Bullet.rails_logger = true # Railsのログに出力
    # Bullet.honeybadger = true # Honeybadgerに通知
    Bullet.add_footer = true # ページのフッターに情報を追加
  end
end

これで準備は完了です。Railsサーバーを再起動してください。

BulletによるN+1問題の検出と解決

設定が完了したら、先ほどのN+1問題が発生する記事一覧ページにアクセスしてみましょう。すると、ブラウザに以下のようなポップアップアラートが表示されるはずです。(設定による)

Bullet Notification: The request below has N+1 queries detected: GET /articles

USE eager loading detected Article => [:user] Add .includes(:user) to your query.

Call stack: /path/to/app/views/articles/index.html.erb:5:in _app_views_articles_index_html_erb___...

この通知は非常に親切で、以下のことを教えてくれます。

  • どのページで問題が起きたか: GET /articles
  • どのモデルの関連で問題が起きたか: Article => [:user]
  • どう解決すればよいか: Add .includes(:user) to your query.
  • どこで問題のコードが呼び出されたか: app/views/articles/index.html.erb:5

解決策: Eager Loading(事前読み込み)

Bulletが提案してくれた通り、Eager Loading(イーガーローディング、事前読み込み) を使ってこの問題を解決します。Eager Loadingとは、あらかじめ必要になる関連データをまとめて読み込んでおく手法です。

Railsでは主にincludesメソッドを使います。

修正後のコントローラ

ruby
# app/controllers/articles_controller.rb
def index
  # .includes(:user) を追加
  @articles = Article.includes(:user).all
end

includes(:user)を追加するだけで、Railsは賢くクエリを最適化してくれます。このコードが実行されると、発行されるクエリは以下の2つだけになります。

sql
-- 1. すべての記事を取得
SELECT "articles".* FROM "articles"

-- 2. 取得した記事に関連するすべてのユーザーを一度に取得
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, ...) -- 記事のuser_idをまとめて指定

記事が100件あっても、発行されるクエリはたったの2回です。これにより、データベースへのアクセスが劇的に減り、パフォーマンスが大幅に改善します。

修正後に再度ページにアクセスすると、Bulletからの警告は表示されなくなります。

preloadjoinsとの違い

Eager Loadingにはincludesの他にpreloadeager_loadもあります。また、似たような機能を持つjoinsとの使い分けも重要です。

  • includes: ほとんどの場合、これでOK。Railsが状況に応じてpreloadeager_loadのどちらか適切な方を選択してくれます。
  • preload: 関連テーブルを別のクエリで読み込みます(今回解決した例と同じ挙動)。関連テーブルのカラムをwhere句などで使わない場合に適しています。
  • eager_load: LEFT OUTER JOINを使って、1つのクエリで親と子の両方のデータを取得します。関連テーブルのカラムをwhere句で絞り込みたい場合に使います。
  • joins: INNER JOINを使います。関連先のデータが必要なレコードだけを親テーブルから取得したい場合に使います。ただし、joinsだけでは子のデータはセレクトされず、article.userのようにアクセスするとN+1問題が再発するので注意が必要です。(joinspreloadの組み合わせが必要になることもあります)

Bulletはこれらの使い分けについても、「preloadを使いなさい」や「joinspreloadを組み合わせなさい」といった具体的なアドバイスをくれることがあります。

まとめ

N+1問題は、Rails開発者が必ず向き合うことになるパフォーマンスの課題です。Bullet gemは、この見つけにくい問題を開発の早い段階で自動的に検出し、具体的な解決策まで提示してくれる、まさに「相棒」のような存在です。

  • 開発環境には必ずbullet gemを導入しましょう。
  • Bulletの警告が表示されたら、それを無視せず、指示に従ってincludesなどを追加しましょう。
  • パフォーマンスは機能です。快適なアプリケーションを提供するために、N+1問題を意識した開発を心がけましょう。

AI が自動生成した技術記事をまとめたテックブログ