ActiveRecord:Ruby on RailsにおけるN+1クエリ問題の解消
ActiveRecordを用いてRailsのN+1クエリを検出・解決する完全ガイドです。includes、preload、eager_load、そして自動検出ツールを習得します。

N+1クエリは、Railsアプリケーションで最も一般的なパフォーマンス問題の一つです。レコードに対する単純なループが、数百もの不要なSQLクエリを発生させ、レスポンスタイムを大幅に低下させることがあります。本ガイドでは、性能の高いRailsアプリケーションを実現するための検出と解消の手法を扱います。
50件の記事を著者とともに表示するページが、たった1件ではなく51件のSQLクエリを発生させる場合があります。数千人のユーザーを抱える本番環境では、この問題はレスポンスタイムとサーバー負荷の観点で重大なものとなります。
N+1問題の理解
N+1問題は、コードがレコード一覧を取得するために1回のクエリ(1クエリ)を実行し、その後それぞれのレコードに対して関連付けへアクセスするために追加のクエリ(N回)を実行する場合に発生します。「N+1」という名称は、まさにこのパターンを示しています。すなわち、初期クエリ1件 + 関連付けに対するN件のクエリです。
記事と著者を例にした具体的なシナリオが、この問題を分かりやすく示してくれます。最適化を行わない場合、記事の著者にアクセスするたびに新しいSQLクエリが発生します。
# 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の呼び出しごとにデータベースへの追加クエリが発生します。
<!-- app/views/articles/index.html.erb -->
<!-- This view generates N additional queries -->
<% @articles.each do |article| %>
<div class="article">
<h2><%= article.title %></h2>
<!-- Each call generates: SELECT * FROM users WHERE id = ? -->
<p>By <%= article.author.name %></p>
</div>
<% end %>100件の記事に対しては、このコードは101件のSQLクエリを発生させます。Railsのログには繰り返されるクエリにより問題が明確に表示されます。
-- 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 queriesincludesによる解決
includesメソッドはN+1問題を解消するために最も一般的かつ推奨される手段です。ActiveRecordに対して、関連付けを1〜2件の最適化されたクエリでプリロードするように指示します。
# 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
endincludesを使うと、ActiveRecordは記事の件数にかかわらず2件のクエリのみを実行します。1件目で全記事を取得し、2件目で関連する全著者を取得します。
-- Rails logs with includes (only 2 queries)
SELECT "articles".* FROM "articles"
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, ...)入れ子の関連付けについても、ハッシュ構文でプリロードできます。ビューが複数階層の関連付けにアクセスする場面では、この手法が不可欠です。
# 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条件で関連付けを絞り込まない場合に効率的に機能します。
# 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を用いて、データを単一のクエリで読み込みます。関連付けのカラムで絞り込みや並び替えを行う場合は必須となります。
# 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" ASCincludes:賢い振る舞い
includesメソッドは最適な戦略を自動的に選択します。デフォルトではpreloadを使用しますが、WHERE句が関連付けを参照する場合はeager_loadに切り替えます。
# 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問題について警告します。さらに、適切な修正方法も提案してくれます。
# Gemfile
# Bullet detects N+1 in development
group :development do
gem 'bullet'
end開発環境での設定によって、さまざまな警告モードを有効化できます。
# 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
endN+1問題が検出されると、Bulletは推奨される解決策とともに明確なメッセージを表示します。
# 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継続的インテグレーションにおいてBullet.raise = trueを有効化すると、N+1問題が検出された際にテストが失敗します。これによりパフォーマンスのリグレッションを防止できます。
Prosopite:軽量な代替手段
Prosopite gemは、Bulletに比べて軽量で、最小限の設定とテストとの互換性を備えた代替手段を提供します。
# Gemfile
# Prosopite as an alternative to Bullet
group :development, :test do
gem 'prosopite'
end# 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の解消を強制します。
# 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
endStrict Loadingは特定のクエリ単位でも有効化できます。
# 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はデータベースから転送されるデータ量を削減します。
# 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はこのカウントを直接親テーブルに保存します。
# 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マイグレーションでは、デフォルト値付きのカウントカラムを追加します。
# 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クエリなしでカラムを直接読み取ります。
# app/views/articles/index.html.erb
# Using counter cache (no SQL query)
<% @articles.each do |article| %>
<p><%= article.title %> - <%= article.comments_count %> comments</p>
<% end %>Ruby on Railsの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
ベストプラクティスとチェックリスト
体系的なアプローチによって、新規開発でN+1問題を防ぎ、既存コードを段階的に改善できます。
コード作成前のビュー分析
コントローラーのコードを書く前に、ビューを分析してアクセスされる関連付けをすべて洗い出すと有効です。事前の確認により漏れを防げます。
# 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をスコープに集約することで、メンテナンス性が高まり、一貫性が確保されます。
# 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は頻繁なカウントを最適化します
- コントローラーを書く前にビューを分析することで漏れを防げます
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
関連記事

Ruby on Rails 面接質問: 2026年トップ25
Ruby on Railsで最も多く問われる面接質問25選。MVCアーキテクチャ、Active Record、マイグレーション、RSpecテスト、REST APIを詳細な解説とコード例とともに解説。

Ruby on Rails 7: HotwireとTurboによるリアクティブアプリケーション
Rails 7におけるHotwireとTurboの完全ガイド。Turbo Drive、Frames、Streamsを使用してJavaScriptを書かずにリアクティブアプリケーションを構築する方法。

Action CableとWebSocket完全ガイド:2026年Ruby on Rails技術面接対策
Ruby on RailsのAction CableとWebSocketの深掘り解説。コネクション、チャンネル、ブロードキャスト、Rails 8のSolid Cable、Redisによるスケーリング、テスト手法まで面接で問われるポイントをコード例とともに網羅します。