N+1問題、開発者の天敵
N+1問題は、Rails開発においてパフォーマンスを著しく低下させる原因の一つだとお見ます。
「遅いな?」と思っていたら、実はN+1問題だった……なんてことはザラです。
今回は、実際に遭遇したN+1のケースとその解決策をお伝えできればと思います。
こんなところにN+1!?ありがちな発生パターン
N+1問題は、特にアソシエーションをループ処理の中で呼ぶケースで発生しやすい印象です。
❌ ダメな例(N+1問題発生)
class Customer < ApplicationRecord
  has_many :orders
end
class Order < ApplicationRecord
  belongs_to :customer
end
customers = Customer.all
customers.each do |customer|
  puts "#{customer.name} の注文数: #{customer.orders.count}件" # N+1問題発生!
end
コードを見る限り、すごく可読性が高くてよいコードに見えますが、このコードだとcustomer(顧客)の数だけSQLが発行されます(N+1問題)。
つまり、顧客が100人いれば100回、1000人いれば1000回SQLが発行されるわけです...そりゃ遅い!
解決策① eager_load で N+1 を撃退
eager_load を使った改善例
customers = Customer.eager_load(:orders)
customers.each do |customer|
  puts "#{customer.name} の注文数: #{customer.orders.count}件" # SQLの追加発行なし!
end
eager_loadを使うと、内部的にLEFT OUTER JOINを行い、データを一括取得します。
そのため、ループ内でcustomer.orders.countを呼んでも追加のSQLは発行されません。
eager_loadの特徴
とっても便利なeager_loadですが、メリット・デメリットあるので、以下にまとめておきます。
- 
LEFT OUTER JOINを使って関連データを一括取得する - 条件付き検索(例: 
where(orders: { is_deleted: false }))と相性が良い - データ量が多いとJOINの影響でクエリが遅くなることがある
 
解決策② preload でスマートに処理
🚀 preload を使った改善例
customers = Customer.preload(:orders)
customers.each do |customer|
  puts "#{customer.name} の注文数: #{customer.orders.count}件"
end
preloadは、最初にordersを個別のSQLで取得し、メモリにキャッシュします。
その後、ループ内でcustomer.ordersを参照してもSQLが発行されません。
preloadの特徴
こちらに関してもメリット・デメリットあるので以下にまとめます。
- JOINを使わず、関連データを個別のクエリで取得する
 - →データ量が多くてもパフォーマンスが安定しやすい
 - 関連に関する条件が付いている検索((例: where(orders: {status: "confirmed"}))をすると、個別でSQLを発行している関係でエラーが発生することがある。
 
eager_load vs preload どっちを使うべき?
どちらを使うかはケースによります。
- 
条件付きの検索をするなら
eager_load(例:where(posts: { published: true })をしたい場合) - 
データ量が多く、単純な関連データ取得なら
preload(個別のクエリで取得するため、大量データでも安定しやすい) 
この違いを理解して、適切に使い分けましょう。
includesはどう?おすすめしない理由
includesも似たような機能を持っていますが、Railsが自動でeager_loadかpreloadのどちらかを選ぶ仕様になっています。
この挙動が予測しづらいため、明示的にeager_loadかpreloadを使うのがおすすめです。
罠:解決したと思ったら、まだN+1問題が潜んでいた!
eager_loadやpreloadを使っても、ループ内で再度条件をつけてSQLを発行するとN+1が復活します。
❌ これもN+1問題です!
customers = Customer.eager_load(:orders)
customers.each do |customer|
  confirmed_orders = customer.orders.where(status:'confirmed')
  puts "#{customer.name} の確定済み注文数: #{confirmed_orders.count}件"
   # ここで再びSQLが発行される!(N+1問題)
end
selectでメモリ上で処理
customers = Customer.eager_load(:orders)
customers.each do |customer|
  confirmed_orders = customer.orders.select { |order| order.status == 'confirmed' }
  puts "#{customer.name} の確定済み注文数: #{confirmed_orders.count}件"  
  # メモリ上で処理されるのでN+1問題なし!
end
これは、Railsがwhereを使うと毎回SQLを発行する仕様だからです。
解決策としては、メモリ上で処理するselectを活用するのがひとつの手です。
⚡【実例】N+1改善で100倍速くなった話
実際にN+1問題が発生し、パフォーマンスが致命的に低下していた画面がありました。
ページのロード時間が30秒以上かかるレベルでしたが、preloadを適用することで、ロード時間が0.2秒に短縮(約150倍の速度改善)されました!
これはユーザー体験を劇的に向上させることにつながりますし、直した側としても最高に気持ちいいものです。
まとめ
- N+1問題はRailsアプリの天敵!
 - 
eager_loadとpreloadの使い分けがカギ- 条件付き検索をするなら
eager_load - データ量が多い場合は
preload 
 - 条件付き検索をするなら
 - ループ内の
whereに要注意!N+1が再発する罠 - 適切に最適化すれば100倍以上の速度改善も可能!
 
あなたのRailsアプリにもN+1問題、潜んでいませんか?
今すぐチェックして、救い出してあげましょう!