こんにちは!Webアプリケーションエンジニアのレミーです!
この記事はEnigmo Advent Calendar 2025の24日目の記事です。
Ruby on Rails アプリが遅いと感じるのは、ほぼ次の3の原因になります。
- DBクエリが多すぎる(特に N+1、COUNT/EXISTS の使い分けミス、インデックス不足)
- 不要なデータを読み込みすぎる(テーブル全て/重いカラム全て取得、あるいは全部を RAM に書き込む)
- ビューのレンダリング/コールバックが働きすぎる(partial の多用、重いフォーマット処理、不要なコールバック/バリデーションの実行)
この記事では、効果が見えやすいものに絞って、自分が特によく使う最適化10個をまとめます。
1. includes で N+1 クエリを防ぐ
問題: posts の一覧を取得して、view 側で post.user.name や post.comments.size のように関連を参照すると、20件なら20回(あるいはそれ以上)追加クエリが飛ぶ可能性があります。
解決: includes で関連を事前ロードします。
改善前: N+1 が発生
@posts = Post.order(created_at: :desc).limit(20) @posts.each do |post| post.user.name # 毎回 SELECT users... WHERE id = ? が走る可能性 end
改善後: includes を使用
@posts = Post.includes(:user).order(created_at: :desc).limit(20)
Rails は posts に紐づく users を1回のクエリでまとめてロードし、ループ内での追加クエリを防ぎます。
一覧をレンダリングして、ループ内で association を参照する(post.comments、post.user、order.items など)場合ではよく使われます。
重要ポイント: includes には 3 パターンがあり、Rails が状況に応じて選びます。
preload: 常に 2 クエリ(postsとusers)eager_load: 常に LEFT OUTER JOIN(大きい 1 クエリ)includes: Rails が自動判断(preload になる場合も join になる場合もある)
2. 必要なカラムだけ取る: select / pluck
問題: User.all や User.where(...).to_a は 全カラムを引いてきます。bio (text)、settings (jsonb)、avatar_data のような重いカラムも含まれがちです。実際には id と name だけで十分なケースも多いはずです。
解決:
ActiveRecord オブジェクトは欲しい(でも最小限にしたい)
select を使います。
users = User.where(active: true).select(:id, :name) users.first.name # OK
値の配列だけで十分(高速 + allocations 少なめ)
pluck を使います。
ids = User.where(active: true).pluck(:id) pairs = User.where(active: true).pluck(:id, :name) # [[1, "A"], [2, "B"]]
DB処理時間の短縮、返ってくるデータ量の削減、Ruby 側の allocations 削減。
dropdown/select box で id と name だけが必要な時とか、バックグラウンドジョブで処理対象 id だけが必要な時などが使われます。
3. 存在するかどうかの確認なら exists? を使う
問題: relation に対して any? / present? で存在チェックをすると、不要にレコードを読み込んだり、最適でないクエリになったりすることがあります。
解決: exists? は EXISTS を使うため、目的に対して効率的になりやすいです。
改善前: 不要なロードが起こり得る
User.where(email: email).any?
改善後: EXISTS を使う
User.exists?(email: email)
メール重複チェック、ユーザーが注文を持っているか、対象レコードが既にあるか、などの場合に使われます。
4. count / size / length を正しく使い分ける
| メソッド | DB クエリは走る? | どんなクエリ | レコードをロードする? | 使いどころ |
|---|---|---|---|---|
| count | あり(常に) | SELECT COUNT(*) |
なし | DB から正確な件数が必要なとき |
| length | 未ロードなら走る | SELECT * |
あり(全件ロード) | すでに records がロード済みだと確実できる時だけ |
| size | 状況による | COUNT(*) または なし |
自動 | ActiveRecord / association では基本これが安全 |
association がロード済みか不明なときは、次のように size が安全です。
comments_count = post.comments.size
view で association の件数を表示するなら、まずは size(または counter cache)を優先。
5. よく絞り込み/ソートするカラムに Index を貼る
問題: WHERE user_id = ... などがインデックスがないと、DB がテーブル全体をスキャンして重くなりがちです。
解決: WHERE / JOIN / ORDER BY によく出てくるカラムに index を追加します。
例:
add_index :orders, :user_id add_index :users, :email, unique: true add_index :orders, [:user_id, :created_at]
インデックス選びの目安:
WHERE/JOINによく出るカラム: index を追加ORDER BYと filter がセットでよく出る:複合インデックス(例[:user_id, :created_at])を追加- email/username のようなユニーク値:
unique: true
確認方法: ログで遅いクエリを見つける、DB で EXPLAIN を実行して index が使われているか確認。
注意: index は読み込みを速くしますが、書き込みは少し遅くなる傾向があります。
6. 大量データは batch で処理する: find_each / in_batches
問題: User.where(...).each は全件を RAM に書き込む可能性があります。件数が多い(数万〜数百万)と、メモリが不足になって、worker/job が落ちる原因になります。
解決: find_each でバッチ処理します。
find_each は 主キーによるページングで、メモリには 1 バッチ分だけ保持します。
User.where(active: true).find_each(batch_size: 1000) do |user| # user を1件ずつ処理 end
バッチ単位で処理したい(特に一括更新)なら in_batches が便利です。
User.where(active: true).in_batches(of: 1000) do |relation| relation.update_all(flag: true) end
rake タスク、データの移行作業、数万件以上のレコードを扱うジョブなどに使われます。
7. コールバックが不要なら bulk update/delete: update_all / delete_all
問題: 1万件を each { update } すると、1万回のクエリに加えて validations/callbacks が走ります。場合によってはメール送信なども巻き込まれて重くなります。
解決: 1クエリでまとめて処理します。
# 一括更新 User.where(id: ids).update_all(active: false) # 一括削除 Log.where("created_at < ?", 30.days.ago).delete_all
重要: update_all / delete_all は バリデーションとコールバックを完全にスキップします。
フラグを一括変更、単純なデータ修正、ログの削除などに使われます。
8. association の件数表示には counter cache を使う
問題: 一覧で「コメント数」を表示するために post.comments.count を多用すると重くなります。includes(:comments) にしても comments が多いとそれ自体が重くなることがあります。
解決: comments_count のようなカラムに件数を保持します(counter cache)。
# posts に comments_count を追加 add_column :posts, :comments_count, :integer, default: 0, null: false # counter cache を有効化 class Comment < ApplicationRecord belongs_to :post, counter_cache: true end
以降は post.comments_count を使えます。
これは、一覧ページや管理画面など「件数表示」があちこちに出てくる画面で特に効きます。
注意: 読み込みは非常に速くなりますが、コメント作成/削除時に post 側のカラム更新が 1 回増えます。
9. 高コストな処理は Rails.cache.fetch でキャッシュする
問題: 重いクエリや計算(トップの記事、統計、設定値など)を毎リクエスト再計算してしまう。
解決: TTLと分かりやすい key を持ったキャッシュを使います。
top_posts = Rails.cache.fetch(["top_posts", Date.current], expires_in: 10.minutes) do Post.published.order(score: :desc).limit(20).pluck(:id, :title) end
キー設計は ["feature_name", version, params...] の配列にすると、管理しやすいです。expires_in も付けて意図せず永続化する事故を避けましょう。
10. view をキャッシュする(fragment / collection caching)
問題: DB はそこまで遅くないのに、partial が多い、フォーマット処理が重いなどで view のレンダリングが遅くなる。
解決1: record 単位の fragment cache
<% @posts.each do |post| %> <% cache(post) do %> <%= render "posts/post", post: post %> <% end %> <% end %>
Railsはレコードのcache key(バージョン付き)を使うので、post が更新されるとキーも変わり、自然に無効化されます。
解決2: collection caching(短くて速い)
<%= render partial: "posts/post", collection: @posts, cached: true %>
レンダリング時間とCPU使用量が大きく減ります。
Rails が遅い原因は、framework そのものというより、余計なことをしてしまっているコードにあることがほとんどです。まずは上の基本最適化から入れるだけで、複雑なキャッシュ設計やサーバー増強をしなくても、速くなるケースが珍しくありません。
明日の記事の担当はエンジニアの嘉松さんです。お楽しみに。