【Rails】重いページが劇的に改善!運用中RailsアプリにおけるN+1問題との戦いと解決のベストプラクティス

2026/05/11

Ruby on Rails
Performance
Database

こんにちは。

Webアプリケーションを長期間運用していると、次第に直面するのが「ページの表示速度が遅い」というパフォーマンスの問題です。特にRuby on RailsなどのORマッパー(ActiveRecord)を採用しているフレームワークでは、気づかないうちに大量のデータベースクエリが発行されてしまう 「N+1問題」 が頻発しがちです。

先日、私たちが運用しているRailsアプリケーションでも、特定の詳細ページでレスポンスタイムが悪化していることが判明しました。

この記事では、運用中のアプリケーションでN+1問題をどのように発見し、どのように根本解決していったのか、そのベストプラクティスをご紹介します。


1. ボトルネックの特定:なぜページが重いのか?

詳細ページにアクセスした際のサーバーログを確認したところ、以下のような状態になっていました。

User Load (1.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
Item Load (0.8ms)  SELECT "items".* FROM "items" WHERE "items"."user_id" = 1
Category Load (0.5ms)  SELECT "categories".* FROM "categories" WHERE "categories"."id" = 1 LIMIT 1
Category Load (0.4ms)  SELECT "categories".* FROM "categories" WHERE "categories"."id" = 2 LIMIT 1
... (これが数十回続く)

見事なまでのN+1問題です。一覧や詳細ビューで関連レコード(この場合はカテゴリーや画像などのアソシエーション)をループで呼び出す際、一つひとつのレコードに対して個別のSQLクエリが発行されてしまっていました。データ量が増えれば増えるほど、雪だるま式にパフォーマンスが悪化します。

2. bullet gemの導入による自動検知

手動でログを追うのは限界があるため、開発環境にN+1問題の検知ツールである bullet gem を導入しました。

# Gemfile
group :development do
  gem 'bullet'
end

設定ファイル(config/environments/development.rb)でアラートを有効にします。

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true      # ブラウザにJavaScriptのアラートを表示
  Bullet.bullet_logger = true # log/bullet.log に出力
  Bullet.console = true    # ブラウザのコンソールに表示
  Bullet.rails_logger = true # サーバーのログに出力
end

これを導入することで、開発中にページをリロードするだけで「どのファイルの何行目で、どのアソシエーションがN+1を引き起こしているか」をブラウザ画面に直接警告してくれるようになりました。これにより、パフォーマンス悪化の主要因となっている箇所(高インパクトなページ)を効率よく洗い出すフローが確立できました。

3. includes と eager_load による根本解決

問題箇所が特定できたので、コントローラーのクエリを修正します。

修正前(N+1が発生するコード)

def show
  @item = Item.find(params[:id])
  # ビュー側で @item.categories を呼ぶたびにクエリが発行される
end

修正後(Eager Loadingを利用)

def show
  @item = Item.includes(:categories, :images).find(params[:id])
end

includes を使うことで、Railsが裏側で関連データを事前にまとめて取得(Eager Loading)してくれます。先ほど数十回発行されていたクエリが、たった2回のクエリ(Itemの取得と、それに関連するCategoriesのIN句を用いた一括取得)に削減されました。

※ includes と eager_load の使い分け

状況に応じて eager_load (または joins)を明示的に使うこともありました。

  • includes: 基本的に推奨。Railsがよしなに preload (クエリを分ける)か eager_load (LEFT OUTER JOINで1つの巨大なクエリにする)かを選んでくれます。
  • eager_load: 関連先のテーブルの値で絞り込み(where)や並び替えを行いたい場合は、JOINが必須となるためこちらを明示的に指定します。

4. 改善の結果と今後の運用

これらの修正を行い、テスト(spec)を通した上で本番環境へデプロイした結果、該当ページのデータベース応答時間は劇的に改善し、体感速度も驚くほど速くなりました。

また、単発の修正で終わらせるのではなく、今後の開発サイクルにおいても 「機能追加時には必ず bullet の警告が出ないか確認する」「CIで極端なクエリ増を検知する」 といったパフォーマンス監視の文化をチーム内に根付かせることができました。

まとめ

RailsにおけるN+1問題は、開発初期(データ量が少ない時期)には見過ごされがちですが、本番運用においては致命的なパフォーマンス劣化を招きます。

  1. bullet gem を導入し、N+1を可視化・自動検知する仕組みを作る
  2. インパクトの大きいページから優先して対処する
  3. includeseager_load を適切に使ってクエリを最適化する

このステップを踏むことで、保守性の高いサクサク動くアプリケーションを維持できるようになります。同じような問題に直面している方の参考になれば幸いです!

Related Posts

[Rails]mySQL8.0のデフォルトのcollationとActiveRecordの差分に困った話

[Rails]mySQL8.0のデフォルトのcollationとActiveRecordの差分に困った話