# ActiveRecord:Ruby on RailsにおけるN+1クエリ問題の解消 > ActiveRecordを用いてRailsのN+1クエリを検出・解決する完全ガイドです。includes、preload、eager_load、そして自動検出ツールを習得します。 - Published: 2026-02-23 - Updated: 2026-04-29 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, activerecord, performance, n+1 queries, sql optimization - Reading time: 10 min --- N+1クエリは、Railsアプリケーションで最も一般的なパフォーマンス問題の一つです。レコードに対する単純なループが、数百もの不要なSQLクエリを発生させ、レスポンスタイムを大幅に低下させることがあります。本ガイドでは、性能の高いRailsアプリケーションを実現するための検出と解消の手法を扱います。 > **本番環境への影響** > > 50件の記事を著者とともに表示するページが、たった1件ではなく51件のSQLクエリを発生させる場合があります。数千人のユーザーを抱える本番環境では、この問題はレスポンスタイムとサーバー負荷の観点で重大なものとなります。 ## N+1問題の理解 N+1問題は、コードがレコード一覧を取得するために1回のクエリ(1クエリ)を実行し、その後それぞれのレコードに対して関連付けへアクセスするために追加のクエリ(N回)を実行する場合に発生します。「N+1」という名称は、まさにこのパターンを示しています。すなわち、初期クエリ1件 + 関連付けに対するN件のクエリです。 記事と著者を例にした具体的なシナリオが、この問題を分かりやすく示してくれます。最適化を行わない場合、記事の著者にアクセスするたびに新しいSQLクエリが発生します。 ```ruby # app/controllers/articles_controller.rb # Example code generating an N+1 problem class ArticlesController < ApplicationController def index # 1 query: SELECT * FROM articles @articles = Article.all end end ``` ビュー側では、`article.author`の呼び出しごとにデータベースへの追加クエリが発生します。 ```erb <% @articles.each do |article| %>

<%= article.title %>

By <%= article.author.name %>

<% end %> ``` 100件の記事に対しては、このコードは101件のSQLクエリを発生させます。Railsのログには繰り返されるクエリにより問題が明確に表示されます。 ```sql -- Rails logs showing the N+1 problem -- 1 initial query SELECT "articles".* FROM "articles" -- N queries for authors (repeated for each article) 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 -- ... 97 more queries ``` ## includesによる解決 `includes`メソッドはN+1問題を解消するために最も一般的かつ推奨される手段です。ActiveRecordに対して、関連付けを1〜2件の最適化されたクエリでプリロードするように指示します。 ```ruby # app/controllers/articles_controller.rb # Solution with includes - preloading authors class ArticlesController < ApplicationController def index # Preloads authors with articles # Generates only 2 queries instead of N+1 @articles = Article.includes(:author).all end end ``` `includes`を使うと、ActiveRecordは記事の件数にかかわらず2件のクエリのみを実行します。1件目で全記事を取得し、2件目で関連する全著者を取得します。 ```sql -- Rails logs with includes (only 2 queries) SELECT "articles".* FROM "articles" SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...) ``` 入れ子の関連付けについても、ハッシュ構文でプリロードできます。ビューが複数階層の関連付けにアクセスする場面では、この手法が不可欠です。 ```ruby # app/controllers/articles_controller.rb # Preloading nested associations class ArticlesController < ApplicationController def index # Preloads author -> company and all comments @articles = Article.includes(author: :company, comments: :user) end end ``` > **黄金律** > > ビューがループの中で関連付けにアクセスする場合、その関連付けはコントローラーで`includes`によりプリロードしておく必要があります。ビュー内の関連付けへのアクセスパターンは常に確認することが重要です。 ## includes・preload・eager_loadの違い Railsは関連付けのプリロードに対して3種類のメソッドを提供しています。それぞれ異なるSQL戦略を採用しており、用途も異なります。 ### preload:別々のクエリ `preload`メソッドは関連付けごとに常に別々のクエリを実行します。WHERE条件で関連付けを絞り込まない場合に効率的に機能します。 ```ruby # app/models/article.rb # preload always uses separate queries class Article < ApplicationRecord scope :with_authors, -> { preload(:author) } end # Usage in controller @articles = Article.with_authors.limit(20) # SQL generated: # SELECT "articles".* FROM "articles" LIMIT 20 # SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, ...) ``` ### eager_load:LEFT OUTER JOIN `eager_load`メソッドはLEFT OUTER JOINを用いて、データを単一のクエリで読み込みます。関連付けのカラムで絞り込みや並び替えを行う場合は必須となります。 ```ruby # app/controllers/articles_controller.rb # eager_load allows filtering on associations class ArticlesController < ApplicationController def verified_authors # Filters articles by author status # Requires eager_load because WHERE references users @articles = Article.eager_load(:author) .where(users: { verified: true }) .order("users.name ASC") end end # SQL generated (single query with JOIN): # SELECT "articles"."id", "articles"."title", ... # FROM "articles" # LEFT OUTER JOIN "users" ON "users"."id" = "articles"."author_id" # WHERE "users"."verified" = TRUE # ORDER BY "users"."name" ASC ``` ### includes:賢い振る舞い `includes`メソッドは最適な戦略を自動的に選択します。デフォルトでは`preload`を使用しますが、WHERE句が関連付けを参照する場合は`eager_load`に切り替えます。 ```ruby # app/controllers/articles_controller.rb # includes adapts automatically to context class ArticlesController < ApplicationController def index # No condition on association: uses preload (2 queries) @articles = Article.includes(:author).all end def by_verified_authors # With condition on association: uses eager_load (JOIN) @articles = Article.includes(:author) .where(users: { verified: true }) end end ``` 以下の表は、3つのメソッドの違いをまとめたものです。 | メソッド | SQL戦略 | ユースケース | |----------|---------|--------------| | `preload` | 別々のクエリ | シンプルなプリロード、絞り込みなし | | `eager_load` | LEFT OUTER JOIN | 関連付けでの絞り込みや並び替え | | `includes` | 自動 | 一般用途、推奨されるデフォルト | ## N+1の自動検出 N+1問題を手作業で見つけるのは骨の折れる作業であり、見落としも発生しやすくなります。複数のツールがこの検出を開発環境やCIで自動化します。 ### Bullet:リアルタイム検出 Bullet gemはSQLクエリをリアルタイムに解析し、検出されたN+1問題について警告します。さらに、適切な修正方法も提案してくれます。 ```ruby # Gemfile # Bullet detects N+1 in development group :development do gem 'bullet' end ``` 開発環境での設定によって、さまざまな警告モードを有効化できます。 ```ruby # config/environments/development.rb # Bullet configuration to detect N+1 Rails.application.configure do config.after_initialize do Bullet.enable = true # Display JavaScript alert in browser Bullet.alert = true # Add footer with details Bullet.bullet_logger = true # Display in Rails logs Bullet.rails_logger = true # Raise exception (useful in CI) Bullet.raise = false end end ``` N+1問題が検出されると、Bulletは推奨される解決策とともに明確なメッセージを表示します。 ```text # Example Bullet alert in logs USE eager loading detected Article => [:author] Add to your query: .includes([:author]) Call stack: /app/views/articles/index.html.erb:5 ``` > **CI環境でのBullet** > > 継続的インテグレーションにおいて`Bullet.raise = true`を有効化すると、N+1問題が検出された際にテストが失敗します。これによりパフォーマンスのリグレッションを防止できます。 ### Prosopite:軽量な代替手段 Prosopite gemは、Bulletに比べて軽量で、最小限の設定とテストとの互換性を備えた代替手段を提供します。 ```ruby # Gemfile # Prosopite as an alternative to Bullet group :development, :test do gem 'prosopite' end ``` ```ruby # config/environments/development.rb # Prosopite configuration Rails.application.configure do config.after_initialize do Prosopite.rails_logger = true Prosopite.raise = Rails.env.test? end end ``` ## 高度な最適化テクニック 基本的な手法以外にも、ActiveRecordクエリの最適化を細かく調整するための複数の技術があります。 ### Strict Loading:デフォルトでの予防 Rails 6.1以降では、プリロードされていない関連付けにアクセスした際に例外を発生させるStrict Loadingモードが提供されています。この予防的な手法は、開発段階でN+1の解消を強制します。 ```ruby # app/models/article.rb # Enable strict loading by default on the model class Article < ApplicationRecord # Any non-preloaded association access raises an exception self.strict_loading_by_default = true belongs_to :author has_many :comments end ``` Strict Loadingは特定のクエリ単位でも有効化できます。 ```ruby # app/controllers/articles_controller.rb # Strict loading on a specific query class ArticlesController < ApplicationController def index # Raises StrictLoadingViolationError if a non-included # association is accessed @articles = Article.strict_loading.includes(:author) end end ``` ### 部分データのためのSelectとPluck 特定のカラムだけが必要な場合、`select`と`pluck`はデータベースから転送されるデータ量を削減します。 ```ruby # app/controllers/reports_controller.rb # Optimization with select and pluck class ReportsController < ApplicationController def titles_only # select returns Article objects with only id and title @articles = Article.select(:id, :title) end def title_array # pluck returns an Array of values, not AR objects # More performant when only values are needed @titles = Article.pluck(:title) # => ["First article", "Second article", ...] end end ``` ### カウントのためのCounter Cache 関連付けのカウント(`article.comments.count`)は呼び出しのたびにSQLクエリを発生させます。Counter cacheはこのカウントを直接親テーブルに保存します。 ```ruby # app/models/comment.rb # Counter cache configuration class Comment < ApplicationRecord # Rails automatically maintains the counter in articles.comments_count belongs_to :article, counter_cache: true end ``` マイグレーションでは、デフォルト値付きのカウントカラムを追加します。 ```ruby # db/migrate/20260223_add_comments_count_to_articles.rb # Migration to add counter cache class AddCommentsCountToArticles < ActiveRecord::Migration[7.1] def change add_column :articles, :comments_count, :integer, default: 0, null: false # Initialize counters for existing data Article.find_each do |article| Article.reset_counters(article.id, :comments) end end end ``` この設定の後は、`article.comments_count`が追加のSQLクエリなしでカラムを直接読み取ります。 ```ruby # app/views/articles/index.html.erb # Using counter cache (no SQL query) <% @articles.each do |article| %>

<%= article.title %> - <%= article.comments_count %> comments

<% end %> ``` ## ベストプラクティスとチェックリスト 体系的なアプローチによって、新規開発でN+1問題を防ぎ、既存コードを段階的に改善できます。 ### コード作成前のビュー分析 コントローラーのコードを書く前に、ビューを分析してアクセスされる関連付けをすべて洗い出すと有効です。事前の確認により漏れを防げます。 ```ruby # app/controllers/articles_controller.rb # Pre-analyze view to identify required includes class ArticlesController < ApplicationController def show # View accesses: author, author.company, comments, comments.user # All these must be preloaded @article = Article.includes( author: :company, comments: :user ).find(params[:id]) end end ``` ### 再利用可能なスコープ 頻繁に使うincludesをスコープに集約することで、メンテナンス性が高まり、一貫性が確保されます。 ```ruby # app/models/article.rb # Reusable scopes for preloading class Article < ApplicationRecord # Scope for list display scope :with_author, -> { includes(:author) } # Scope for detailed display scope :with_full_details, -> { includes( author: :company, comments: { user: :avatar_attachment }, tags: [] ) } # Scope for admin with all relations scope :for_admin, -> { includes(:author, :comments, :tags, :category) .with_attached_cover_image } end ``` ### 予防チェックリスト このチェックリストは、N+1問題を回避するための重要な確認項目を整理しています。 - 開発環境でBulletまたはProsopiteを導入し設定する - 回帰を阻止するためにCIでBullet.raiseを有効化する - コントローラー作成前にビューを分析し関連付けを特定する - デフォルトで`includes`を使い、関連付けでの絞り込みがあれば`eager_load`を使う - 頻出するプリロードパターンには再利用可能なスコープを作成する - 重要なモデルにはStrict Loadingを適用する - 頻繁なカウントにはCounter Cacheを追加する - 開発環境でSQLログを定期的に確認する > **過剰なプリロードへの注意** > > 関連付けを過剰にプリロードすると、不必要にメモリを消費します。ビューが実際に使用する関連付けだけをプリロードすべきです。Bulletのようなツールは「unused eager loading」も検出します。 ## まとめ N+1クエリはRailsアプリケーションで重大なパフォーマンス問題ですが、容易に予防できます。自動検出ツールと優れた開発プラクティスを組み合わせることで、この問題を効果的に解消できます。 **重要なポイント:** - `includes`は関連付けをプリロードすることで多くのN+1ケースを解決します - 関連付けで絞り込みや並び替えを行う場合は`eager_load`が必要です - BulletとProsopiteは開発環境で問題を自動的に検出します - Strict Loadingは例外を発生させてN+1を防ぎます - Counter Cacheは頻繁なカウントを最適化します - コントローラーを書く前にビューを分析することで漏れを防げます --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/ruby-on-rails/activerecord-fixing-n-plus-1-query-problems