YASD-TECH
YASD TECH
# Rails

eager_load・preload・includesの違いと使い分け

投稿日:2026/7/27

更新日:2026/7/27

ttitleImage

eager_load・preload・includes の違いと使い分け

ActiveRecord で N+1 問題を潰すための3つのメソッドのまとめ。
どれも「関連(association)を事前に読み込んでキャッシュする」点は同じで、発行するクエリの形が違う

用語の前提だけ先に置いておく。

用語 意味
association(関連) 関連先のテーブルのこと。User.preload(:posts) でいう posts にあたる
キャッシュ 取得済みのレコードをメモリに保持しておき、2回目以降の参照でクエリを発行しないこと
eager loading ループに入る前に関連をまとめて読み込んでおくこと。preload / eager_load / includes がやっているのはこれ

1. N+1 問題とは

ループの中で1件ずつクエリが飛び、クエリ数がレコード数に比例して増えてしまう問題。

会員(users)と投稿(posts)が 1対多 で関連づいているとする。

ruby
class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

投稿一覧で「投稿のタイトル」と「投稿者名」を出す。

ruby
# posts_controller.rb
def index
  @posts = Post.all
end
erb
<%# views/posts/index.html.erb %>
<% @posts.each do |post| %>
  <%= post.title %>
  <%= post.user.name %>   <%# ← ここで毎回クエリが飛ぶ %>
<% end %>

発行されるクエリ:

sql
-- 投稿を取る 1 回
SELECT "posts".* FROM "posts"

-- 投稿の件数(N)だけ繰り返される
SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1
SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1
SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1
...

1 + N 回発行されるので N+1 問題と呼ばれる。1クエリ自体は 0.1ms でも、100件あれば往復コストが積み上がってレスポンスが目に見えて遅くなる。


2. preload — 関連ごとにクエリを分ける

指定した関連を別クエリで引いてキャッシュする。まず主テーブルを取り、その idIN 句に並べて関連を取る。

ruby
User.preload(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)

2クエリで済み、ループ中に追加のクエリは飛ばない。

  • JOIN しないので、主テーブルの行が関連の件数だけ複製されない(結果セットが膨らまない)
  • 逆に 関連先のカラムで絞り込めないUser.preload(:posts).where(posts: { published: true }) はエラーになる

注意: IN 句が大きくなりすぎるケース
主テーブルのレコード数が多いと IN (...) に大量の値が並び、SQL 文自体が肥大化する。
MySQL では max_allowed_packet を超えると Mysql2::Error になるなど、DB 側の設定値に当たることがある。
件数が読めない一覧では find_each などでバッチに区切るか、そもそも件数を絞る。


3. eager_load — LEFT OUTER JOIN で1発で取る

左外部結合(LEFT OUTER JOIN)で主テーブルと関連を1クエリにまとめて取得する。

ruby
Post.all.eager_load(:user)
# SELECT "posts"."id" AS t0_r0, "posts"."title" AS t0_r1, ... ,
#        "users"."id" AS t1_r0, "users"."name" AS t1_r1, ...
# FROM "posts" LEFT OUTER JOIN "users" ON "users"."id" = "posts"."user_id"

左外部結合は、2つのテーブルを結合キーで対応づけて1行に連結するもので、右側に一致する相手がいなかった行も残るのが特徴(この場合 user が nil の投稿も消えない)。INNER JOIN だと消えてしまうので、eager loading にはこちらが使われる。

  • 関連先のカラムで絞り込める。これが preload との決定的な差
    ruby
    Post.eager_load(:user).where(users: { active: true })
    
  • 1クエリで済むが、has_many を JOIN すると主テーブルの行が関連の件数だけ複製される
    ユーザー1人が投稿100件持っていれば、そのユーザーのカラムが100行分転送される
  • カラムに t0_r0 のような別名が振られるのは、両テーブルの idname の衝突を避けるため

4. includes — preload と eager_load を自動で振り分ける

includes は自前のクエリ戦略を持たず、状況に応じて preload か eager_load のどちらかに落ちる。

  1. 基本は preload(クエリ分割)
  2. 次のいずれかに当てはまると eager_load(LEFT OUTER JOIN)に切り替わる
    • includes した関連に対して references または joins を呼んでいる
    • 関連先のカラムを条件に使っている(ハッシュ条件は自動検出されるが、文字列条件は references が必要)
    • 同じクエリ内で eager_load を明示的に呼んでいる
ruby
# preload 相当(2クエリ)
User.includes(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)

# eager_load 相当(LEFT OUTER JOIN 1クエリ)
User.includes(:posts).references(:posts).where("posts.published = ?", true)

5. (参考)joins — キャッシュしない結合

joins はデフォルトで INNER JOIN を行う。他の3つとの最大の違いは 関連をキャッシュしないこと。

ruby
Post.joins(:user).where(users: { active: true })
# SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id"
#   WHERE "users"."active" = TRUE

キャッシュしないので eager loading の代わりにはならない(ループ中に post.user を触れば N+1 が復活する)。
一方で ActiveRecord オブジェクトを生成しない分、メモリ消費は抑えられる

関連先のレコードにアクセスせず、単に絞り込みたいだけなら joins が適している。


6. 使い分け

クエリの形 関連先で絞り込み キャッシュ
preload 関連ごとに分割
eager_load LEFT OUTER JOIN で1本
includes 状況により上のどちらか
joins INNER JOIN で1本

includes は避けたほうがよいケースが多い

  • 関連が複数あるとき、個別に最適化できない

    ruby
    Post.includes(:user, :comments)
    

    user は eager_load、comments は preload」という振り分けができない。
    1つでも eager_load すべき関連があれば、すべて LEFT OUTER JOIN にまとめられる
    本来クエリを分けたほうが速い関連まで JOIN に巻き込まれ、かえって遅くなることがある。

  • どちらに落ちるかがコード上から読み取りにくい

    条件次第でクエリが変わるため、preload と eager_load の判定基準を正確に把握していないと、
    データ量が増えたときに挙動をコントロールしづらい。

意図が明確なぶん、preload / eager_load を明示的に書き分けるほうが安全。

preload を選ぶケース

  • has_many の関連を事前読み込みする場合
  • 複数の関連をまとめて事前読み込みする場合

eager_load は常に結合するため、has_many を含むとデータ量が大きいときに重いクエリになる。
クエリを分割したほうがレスポンスが速いことが多い。

eager_load を選ぶケース

  • 関連先のカラムで絞り込みたい場合(preload では不可能なので実質こちら一択)
  • has_one / belongs_to のように 1:1・N:1 の関連で、1クエリで取ったほうが効率が良い場合

1:1・N:1 なら外部結合しても取得行数は増えないので、クエリを分けるより 1 本で取るほうが速い。

事前読み込みしないケース

そもそも事前読み込みが要らない場面のほうが多い。ループ中に関連を触らないなら付けないのが基本で、
使わない関連を preload するのは、無駄なクエリと無駄なオブジェクト生成を増やすだけになる。

ruby
# 自テーブルのカラムしか表示しない一覧。preload は不要
Post.order(created_at: :desc).page(params[:page])

# 絞り込みたいだけで post.user は触らない → joins(キャッシュ不要)
Post.joins(:user).where(users: { active: true })

# そもそもモデルを組み立てない
Post.count
Post.pluck(:title)

Post.all のような素の取得が問題になるのは、次の2つのときだけ。

  • each の中で関連を触っている(= N+1 の本体)
  • ページネーションなしで全件をビューに渡している(関連の有無に関係なく、メモリと転送量で詰まる)

なお Model.all は配列ではなく ActiveRecord::Relation を返し、この時点ではクエリが飛ばない。
count / pluck / find_each の起点や、scope 内の条件分岐の初期値として普通に使う。
→ 遅延評価そのものについては別ノート ActiveRecord Relation の遅延評価 に切り出す。

strict_loading で「触る/触らない」を強制する

Rails 6.1 以降、事前読み込みしていない関連にアクセスした時点で例外を投げさせられる。
「N+1 に気付かないまま本番でレコードが増えて顕在化する」という一番厄介なパターンを、開発時に落とせる。

ruby
Post.strict_loading.each do |post|
  post.user.name   # ActiveRecord::StrictLoadingViolationError
end

モデル単位・関連単位でも指定できる。

ruby
class Post < ApplicationRecord
  self.strict_loading_by_default = true      # このモデル全体に適用
  has_many :comments, strict_loading: true   # この関連だけ適用
end

アプリ全体に効かせるなら設定で入れる。開発・テスト環境だけ例外にして、本番はログに留める運用も取れる。

ruby
# config/environments/development.rb
config.active_record.strict_loading_by_default = true
config.active_record.action_on_strict_loading_violation = :raise  # :log にすると警告のみ

まとめ

  • N+1 は「ループ中に関連を触ると1件ずつクエリが飛ぶ」問題。事前読み込みで潰す
  • preload = クエリ分割 / eager_load = LEFT OUTER JOIN / includes = その自動振り分け / joins = 結合だけでキャッシュなし
  • 迷ったら「関連先で絞り込むか?」で決める。絞り込むなら eager_load、しないなら preload
  • has_many を eager_load すると結果セットが膨らむ、preload は IN 句が膨らむ。どちらもレコード数が増えてから顕在化する
  • includes は便利だが、複数関連を一括最適化できずクエリが読めなくなるため、明示的な使い分けを基本にする
  • 触らない関連は事前読み込みしない。 付けるかどうか自体が使い分けの一部
  • strict_loading を入れておくと、事前読み込み漏れを実行時ではなく開発時に落とせる

参考

Index

  • eager_load・preload・includes の違いと使い分け
  • 1. N+1 問題とは
  • 2. preload — 関連ごとにクエリを分ける
  • 3. eager_load — LEFT OUTER JOIN で1発で取る
  • 4. includes — preload と eager_load を自動で振り分ける
  • 5. (参考)joins — キャッシュしない結合
  • 6. 使い分け
  • includes は避けたほうがよいケースが多い
  • preload を選ぶケース
  • eager_load を選ぶケース
  • 事前読み込みしないケース
  • strict_loading で「触る/触らない」を強制する
  • まとめ
  • 参考