Rails の N+1 問題とは?includes・preload・eager_load の違いと見つけ方

PR
Rails の N+1 問題とは?includes・preload・eager_load の違いと見つけ方
この記事は約20分で読めます。

Rails の N+1 問題とは、1回のクエリ(データベースへの問い合わせ)で N 件のレコードを取ったあと、1件ごとに追加のクエリが走り、N 回ぶん余計にクエリが出てしまうことです。直し方は、あとで使う関連付けを前もって指定して読み込んでおくことで、そのためのメソッドが includes・preload・eager_load の3つです。違いを一言でいうと、preload は関連付けごとに別のクエリで、eager_load は LEFT OUTER JOIN を使った1本のクエリで読み込み、includes はクエリに応じてその2つを使い分けます。

この記事は、Rails で一覧ページを作ったらログに SQL がたくさん並んで困っている、という初心者に向けて、N+1 問題の見つけ方と3つのメソッドの違いを説明します。Rails 8.1 の文書(Rails ガイドと API ドキュメントの 8.1.4 の版)に沿って書いています。8.1.4 は 2026年9月24日に公開された、2026年10月時点の安定版です。Rails そのものについては、人気プログラミング言語&フレームワークの記事の「Rails (Ruby on Rails)」の項で紹介しています。

この記事でわかること
  • Rails の N+1 問題とは何か(公式ガイドの「本と著者」の例で)
  • ログと strict_loading を使った N+1 の見つけ方と、それぞれが使える Rails の版
  • includes・preload・eager_load の違い(公式ガイドの SQL を並べて)
  • joins との違いと、公式の N+1 の解決策の一覧に joins が入っていないこと
  • 迷ったときの公式の推奨

Rails の N+1 問題とは?

Rails ガイド(英語版の 8.1.4)は、N+1 クエリ問題を次のように説明しています。1回のクエリで N 件(N は 1 より大きい数)のレコードの一覧を取ると、ときに、レコード1件ごとに1回ずつ、合わせて N 回の追加のクエリが走ります。

公式ガイドの例を見てみましょう。本(Book)を10冊取り、1冊ずつ著者(author)の last_name を表示するコードです。

books = Book.limit(10)

books.each do |book|
  puts book.author.last_name
end

出典: Rails ガイド(Active Record クエリ)

ガイドによると、このコードは「10冊の本を探す1回」と「本ごとに著者を読み込む10回」を合わせて、合計11回のクエリを実行します。ガイドは、このコードは一見よさそうに見えるが、問題は実行されるクエリの総数にある、とも書いています。

ポイントは、1冊ごとに、その本の著者を読み込むクエリが1本ずつ出ていることです。このように、関連付け(ここでは本から著者へのつながり)を前もって読み込まずに、使う所で読み込むやり方を、lazy loading(日本語訳では「遅延読み込み」)と呼びます。

API ドキュメントにも、同じ話が載っています。投稿100件のそれぞれで著者を表示すると、データベースへのクエリが101回になります。API ドキュメントによると、eager loading を使うと、クエリの数は101回から2回に減ります。

eager loading(日本語訳では「一括読み込み」)とは、公式の説明では、Active Record で取り出したレコードに関連付いたレコードを、できるだけ効率のよいクエリで読み込む仕組みです。

クエリが何本から「多すぎる」のかという目安は、公式にはまだ書かれていません(2026年10月時点)。

N+1 問題はどうやって見つける?

ここでは、公式のガイドに載っている2つの方法を紹介します。ログで SQL の並びを見る方法と、strict_loading で遅延読み込みが残っていないか確かめる方法です。

ログで SQL の並びを見る

Rails のログは、既定では Rails.root/log/(アプリのフォルダの中の log フォルダ)の下に、アプリが動いている環境の名前を付けたファイルとして作られます。

公式のデバッグのガイドの例では、N+1 のときのログは次のような形です。本体(articles)の SELECT が1本あり、そのあとに、関連先(comments)の SELECT が article_id だけを変えて何本も並びます。

Article Load (0.4ms)  SELECT "articles".* FROM "articles"
Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 1]]
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 2]]
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 3]]

出典: Rails ガイド(デバッグ)(ガイドの例は、コンソール(irb)の画面です)

どのコードから出た SQL なのかは、verbose_query_logs を有効にすると分かります。有効にすると、各 SQL の下に、その SQL を出したメソッドがあるファイル名と行番号が、矢印つきで ↳ app/models/article.rb:5 のように出ます。公式は、これが N+1 クエリが原因の性能の問題を見つけて直すのに役立つ、と書いています。デバッグのガイドは、N+1 クエリを「1つのデータベースクエリが、追加のクエリをたくさん生むこと」と説明しています。

verbose_query_logs は、既定では開発環境で true、ほかの環境で false です。Rails 8.1.4 で新しく作ったアプリの config/environments/development.rb にも、config.active_record.verbose_query_logs = true と書かれています。

本番環境では、この設定を使わないよう公式が勧めています。メソッドの呼び出しの経路(スタックトレース)を作るために Ruby の Kernel#caller を使い、多くのメモリを確保しがちだからです。代わりに、query log tags を使うよう案内しています。query log tags は、SQL にコントローラー名やアクション名などのコメントを付ける仕組みで、公式の例では /*application='Blog',controller='articles',action='index'*/ のようなコメントが SQL の末尾に付きます。設定の config.active_record.query_log_tags_enabled は、既定が false ですが、新しく作ったアプリの development.rb では true になっています。

strict_loading で遅延読み込みを見つける

eager loading をしていても、遅延読み込みがどこかに残っていることがあります。公式のガイドは、遅延読み込みが起きないことを確かめたいときは strict_loading を有効にできる、と書いています。strict_loading を付けて取り出したレコードで、関連付けを遅延読み込みしようとすると、ActiveRecord::StrictLoadingViolationError という例外が出ます。

user = User.strict_loading.first
user.address.city # raises an ActiveRecord::StrictLoadingViolationError

出典: Rails ガイド(Active Record クエリ)

API ドキュメントによると、この例外は、strict loading の印が付いたレコードを遅延読み込みしようとしたときに出ます。直し方は、読む前に eager loading しておくことです。

strict_loading は、範囲を決めて有効にできます。

有効にする範囲公式の書き方
クエリに付けるUser.strict_loading.first
1件のレコードuser.strict_loading!
1つの関連付けhas_many :books, strict_loading: true
1つのモデルモデルの中に self.strict_loading_by_default = true
アプリ全体config.active_record.strict_loading_by_default = true(既定は false)

出典: Rails ガイド(Active Record クエリ)、Rails 6.1.0 の CHANGELOG(モデルごとの書き方)

例外にせず、ログに出すだけにしたいときは、config.active_record.action_on_strict_loading_violation = :log と書きます。既定は、どの環境でも :raise(例外にする)です。

strict_loading! には mode という引数もあります。:n_plus_one_only を指定すると、N+1 クエリにつながる関連付けを遅延読み込みしたときだけ、例外になります。公式の例では、user.comments.to_a は例外にならず、user.comments.first.likes.to_a は例外になります。

user.strict_loading!(mode: :n_plus_one_only)
user.comments.to_a # => [#<Comment:0x00...]
user.comments.first.likes.to_a # raises an ActiveRecord::StrictLoadingViolationError

出典: Rails ガイド(Active Record クエリ)

見つけ方の機能が入った Rails の版(2026年10月時点)

お使いの Rails の版によっては、使えない機能があります。CHANGELOG とリリースノートに書かれている、入った版は次のとおりです。

機能入った版
verbose_query_logsRails 5.2.0(2018年4月9日)
strict_loading(クエリに付ける形、関連付けの strict_loading: true、strict_loading_by_default、action_on_strict_loading_violation)Rails 6.1(2020年12月9日)
strict_loading! の mode: :n_plus_one_only、1件だけ外す strict_loading!(false)Rails 7.0(2021年12月15日)
strict_loading_mode の設定(アプリ全体、またはモデルごと)Rails 8.0(2024年11月7日)

includes・preload・eager_load の違いは?

Active Record では、読み込む関連付けを、前もってすべて指定しておけます。そのためのメソッドが、includes・preload・eager_load の3つです(ガイドの「N+1 クエリ問題の解決策」)。どれも Active Record のメソッドです。Active Record のほかのメソッドの話は、find_or_create_by と find_or_initialize_by の違いの記事でも取り上げています。

ガイドの同じ例(本と著者)で比べると、次のとおりです。

見るところincludespreloadeager_load
読み込み方(公式の説明)できるだけ効率のよいクエリで読み込もうとする。クエリに応じて preload か eager_load を使い分ける関連付けごとに1本のクエリで読み込む指定した関連付けすべてを、LEFT OUTER JOIN で1本のクエリにして読み込む
ガイドの例のクエリの数2本2本1本
読み込む関連付けへの条件付けられる付けられない付けられる
公式の補足条件が無いときは、preload と全く同じ動き条件が無いときの includes と全く同じ動きJOIN で読み込むと、重複したデータを含む行が多くなることがあり、規模が大きいと性能が悪い

includes

includes は、指定した関連付けを、できるだけ効率のよいクエリで読み込もうとするメソッドです。ガイドには、最初の N+1 の例を includes で書き換えた例が載っています。

books = Book.includes(:author).limit(10)
SELECT books.* FROM books
  LIMIT 10

SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

出典: Rails ガイド(Active Record クエリ)

ガイドに載っている SQL は2本です。2本目は、著者の id を IN (…) にまとめて渡して、著者を1回で取っています。ガイドは、元の11本のクエリに対して、これなら2本で済む、と書いています。

API ドキュメントは、includes を「関連付けごとに別のクエリが実行される。ただし、条件のために JOIN が必要な場合は別」と説明しています。また、ガイドは、includes は preload か eager_load をクエリに応じて使い分ける上位のメソッドなので、includes を優先して使うよう書いています。

preload

preload は、指定した関連付けを、関連付けごとに1本のクエリで読み込みます。ガイドは、これは条件が無いときの includes と全く同じ動きだ、と書いています。

books = Book.preload(:author).limit(10)

出典: Rails ガイド(Active Record クエリ)

ガイドに載っている SQL は、includes の例と同じ形で、クエリは2本です。ただし、includes と違って、preload では、読み込む関連付けに条件を付けられません。

eager_load

eager_load は、指定した関連付けすべてを、LEFT OUTER JOIN で1本のクエリにして読み込みます。JOIN は、複数のテーブルをつなぐ SQL の書き方です。

books = Book.eager_load(:author).limit(10)
SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books"
  LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id"
  LIMIT 10

出典: Rails ガイド(Active Record クエリ)

ガイドは、元の11本のクエリに対して、これなら1本で済む、と書いています。SQL の列には、t0_r0 のような別名が付いています。なお、ガイドの SQL は、例ごとに引用符の付け方などが違います(books.* と "books"."id")。この記事では、ガイドのとおりに写しています。

eager_load は、includes と同じように、読み込む関連付けに条件を付けられます。ただし、API ドキュメントには、JOIN で関連付けを読み込むと、重複したデータを含む行が多くなることがあり、規模が大きいと性能が悪い、という注意があります。

includes が JOIN に切り替わるとき

includes は、関連先の条件を付けると、JOIN に切り替わることがあります。ガイドの例では、where にハッシュで関連先の条件を渡すと、LEFT OUTER JOIN を含む1本のクエリになります(joins なら INNER JOIN を使うクエリです)。where の条件が無ければ、ふつうの2本のクエリです。

Author.includes(:books).where(books: { out_of_print: true })

出典: Rails ガイド(Active Record クエリ)

ガイドによると、where が自動で JOIN に切り替えるのは、条件をハッシュで渡したときだけです。SQL の文字列で条件を書くときは、references でテーブルを指定します。

Author.includes(:books).where("books.out_of_print = true").references(:books)

出典: Rails ガイド(Active Record クエリ)

API ドキュメントには、includes には関連付けの名前を、references にはテーブルの名前を渡す、という注意があります。

ただし、includes に条件を付けたときの動き(どんなときに JOIN になるか、どのレコードが返るか)は、公式の文書どうしで書き方が食い違っている箇所があります。この記事では動かして確かめていないので、どちらとも決めません(2026年10月時点)。条件を付けて使うときは、ログに出る SQL で確かめてください。

joins との違いは?

joins は、SQL に JOIN 句を付けるためのメソッドです。ガイドは、JOIN 句を付けるメソッドとして joins と left_outer_joins の2つを挙げていて、joins は INNER JOIN(または独自の JOIN)、left_outer_joins は LEFT OUTER JOIN を使います。ガイドの例です。

Book.joins(:reviews)
SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id

出典: Rails ガイド(Active Record クエリ)

ガイドによると、この SQL は、レビューのある本すべての Book レコードを返します。SELECT の中身は books.* で、本体のテーブル(books)の列だけを取っています。API ドキュメントの User.joins(:posts) の例も、SELECT "users".* と、本体のテーブルの列だけです。

N+1 問題との関係で、公式の文書から確かめられる点は2つです。

  • 公式のガイドが、読み込む関連付けを前もって指定するメソッドとして挙げているのは includes・preload・eager_load の3つで、joins は入っていません
  • joins の SQL は、本体のテーブルの列だけを取っています(上の例)

「joins では N+1 が残る」と言い切る文は、公式にはまだ書かれていません(2026年10月時点)。ガイドの「N+1 クエリ問題の解決策」の節が挙げているのは、includes・preload・eager_load の3つのメソッドです。

そのほか、ガイドには joins について次のことが書かれています。

  • レビューが2件以上ある本は、重複して返ります。重複を消したいときは、Book.joins(:reviews).distinct を使います
  • INNER JOIN なので、結合の条件に合わないレコードは返りません。関連付けがあってもなくても取りたいときは、left_outer_joins(別名 left_joins)を使います

どれを使う?(公式の推奨)

「この場面ではこれ」という細かな使い分けは、公式にはまだ書かれていません(2026年10月時点)。公式のガイドと API ドキュメントが書いていることは、次のとおりです。

  • 迷ったら includes。 includes は、クエリに応じて preload か eager_load を使い分ける上位のメソッドなので、ガイドは includes を優先して使うよう書いています。同じ推奨はガイドの別の箇所にもあり、そこでは、クエリに応じて別々のクエリと LEFT OUTER JOIN を選ぶから、と理由が書かれています
  • 関連付けに条件を付けるクエリには joins。 ガイドは、eager loading した関連付けにも条件を付けられるが、この種のクエリには joins を使うのが推奨、と書いています
  • preload は条件を付けられない。 includes と違って、preload では、読み込む関連付けに条件を付けられません
  • 別のクエリのほうが、単純な JOIN より速いことが多い。 API ドキュメントは、関連付けを別のクエリで読み込むほうが、単純な JOIN よりも性能がよくなることが多い、と書いています。JOIN だと、重複したデータを含む行が多くなることがあり、規模が大きいと性能が悪いためです
  • クエリを減らしても万能ではない。 API ドキュメントは、クエリの数を減らしたからといって、大量のデータを性能への影響なしに取り出せるとは考えないように、と書いています。性能の問題を何でも解決する手段ではありません

よくある質問

includes を書いたのに N+1 が消えないときは?

公式のガイドは、eager loading で N+1 クエリを防げても、遅延読み込みがどこかに残っていることがある、と書いています。残っていないかは、strict_loading を有効にして確かめられます(上の「strict_loading で遅延読み込みを見つける」で説明しています)。

strict_loading が有効なときに遅延読み込みが起きると、ActiveRecord::StrictLoadingViolationError が出ます。API ドキュメントによると、直し方は、読む前に eager loading しておくことです。入れ子になった関連付けは、ハッシュで指定します(書き方は下の「複数・入れ子の関連付けをまとめて読み込むには?」を見てください)。N+1 につながる遅延読み込みのときだけ例外にしたいときは、mode: :n_plus_one_only を使います。

一覧で件数を数えると SQL がたくさん出るときは?

関連付けの件数を数えるメソッドには count・size・length があります。API ドキュメントの説明は次のとおりです。

メソッドAPI ドキュメントの説明
count例のコメントに、SQL で数える、とあります
sizeまだ読み込んでいなければ、SELECT COUNT(*) を実行します。読み込み済みなら、すでにある分を数えます(collection.size を呼びます)
length関連付けを読み込んでから数えます

size と length は、読み込み済みなら同じです。まだ読み込んでいなくて、どうせレコードを使うなら、length のほうがクエリが1本少なくなります。レコードを使わないなら、size のほうが効率がよい、と書かれています。

複数・入れ子の関連付けをまとめて読み込むには?

複数の関連付けは includes に並べて渡し、入れ子の関連付けはハッシュで書きます。次のガイドの例の、1行目が複数の関連付け、2行目が入れ子の関連付けです。

Customer.includes(:orders, :reviews)

Customer.includes(orders: { books: [:supplier, :author] }).find(1)

出典: Rails ガイド(Active Record クエリ)

preload と eager_load も、includes と同じ書き方で、配列・ハッシュ・入れ子のハッシュを渡せます。API ドキュメントによると、includes で読み込むときのクエリの数は、ふつう「1本+指定した関連付けの数」です(ポリモーフィックの belongs_to という特別な関連付けを含むときは例外です)。

preload に条件を付けられる?

公式のガイドでは、付けられないと説明されています。includes と違って、preload では、読み込む関連付けに条件を付けることはできません。eager_load は、includes と同じように、読み込む関連付けに条件を付けられます。ただし、eager loading した関連付けに条件を付けられるとしても、この種のクエリには joins を使うのが推奨だと、ガイドは書いています。

joins で N+1 は解決できる?

公式のガイドが、読み込む関連付けを前もって指定するメソッドとして挙げているのは includes・preload・eager_load の3つで、joins は入っていません。ガイドに載っている Book.joins(:reviews) の SQL は、SELECT books.* と、本体のテーブルの列だけを取っています。「joins では N+1 が残る」と言い切る文は、公式にはまだ書かれていません(2026年10月時点)。

まとめ

  • N+1 問題は、1回のクエリで N 件のレコードを取ったあと、1件ごとに追加のクエリが走り、N 回ぶん余計にクエリが出ること。公式の例では、本10冊と著者で、1+10=11回になる
  • 見つけ方は、ログで、同じ形の SELECT が id だけを変えて並んでいないかを見る。verbose_query_logs で、どのファイルの何行目から出た SQL かが分かる。strict_loading で、遅延読み込みが残っていないか確かめられる
  • 直し方は、使う関連付けを前もって指定すること。そのためのメソッドが includes・preload・eager_load。ガイドの例では、11本のクエリが、includes と preload で2本、eager_load で1本になる
  • 違いは、preload が関連付けごとに別のクエリ、eager_load が LEFT OUTER JOIN の1本のクエリ、includes がクエリに応じてその2つを使い分けること。preload は、読み込む関連付けに条件を付けられない
  • 迷ったら、ガイドは includes を優先するよう書いている。関連付けに条件を付けるクエリには、joins が推奨されている
  • ガイドの N+1 の解決策の節に挙がっているのは3つで、joins は入っていない。クエリを減らしても、大量のデータを取れば遅くなることは変わらない

参考資料

この記事で引用した SQL とコードの例は、Rails ガイド・API ドキュメント・Rails 6.1.0 の CHANGELOG からのもので、一部の行を省いています。Rails ガイドの文は CC BY-SA 4.0 で公開されています。

※この記事は、公式の資料をもとに AI(Claude)で下書きし、2026年10月3日時点の公式の資料と照らして確かめてから公開しています。誤りに気づいたらお問い合わせからお知らせください。

PR
PR
タイトルとURLをコピーしました