3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ActiveRecord のパフォーマンスを考える第一歩!メモリ圧迫を防ぐ基本のキ!

3
Last updated at Posted at 2026-06-07
Page 1 of 15

ActiveRecord のパフォーマンス ― メモリ圧迫・遅延評価・MySQL データ型

📖 概要

この記事は、Rails ガイドや MySQL 公式ドキュメントなどの一次情報をもとに、ActiveRecord のパフォーマンスに関する要点を個人学習用にまとめ直したものです。

ActiveRecord は手軽にデータベースを扱える一方で、仕組みを理解せずに使うとメモリを圧迫したり、無駄なクエリを発行したりしやすいです。本記事では次の3点を扱います。

  • 🗂️ ActiveRecord はオブジェクト化の際にデフォルトで全カラムを取得(SELECT *)し、メモリを圧迫しやすいこと
  • ActiveRecord::Relation が実際にオブジェクト化され、メモリにデータが展開されるタイミング(遅延評価)
  • 🧬 MySQL と連携する際に、パフォーマンスの観点から気を付けたいデータ型

1️⃣ ActiveRecord は全カラムを取得してオブジェクト化する

ActiveRecord は、明示的にカラムを絞らない限りデフォルトで SELECT * を発行する。取得した各行はモデルインスタンスへ変換される。

User.all.to_a
# => SELECT `users`.* FROM `users`

不要なカラムまで取得・オブジェクト化すると、その分メモリを消費する。ここで重要なのは、「取得するカラムを絞ること」と「オブジェクトを生成しないこと」は別のコストだという点。


📦 select vs pluck ― カラム絞り込みの2つの方法

  • 📦 select(:id, :name): 取得カラムを絞るが、モデルインスタンスは生成する(未選択カラムを参照すると ActiveModel::MissingAttributeError
  • 🪶 pluck(:id, :name): モデルインスタンスを作らず、生の値(配列)を返す
User.select(:id, :name)   # User のインスタンス(カラムは絞られる)
User.pluck(:name)         # => ["Alice", "Bob"](ただの配列、オブジェクト生成なし)

一覧表示や集計など「値だけあれば十分」な場面では、pluck のようにオブジェクトを生成しない方法が軽量となる。


📊 大量データを全カラムでロードするとどれくらいメモリを使うか

例として、次のような汎用的な users テーブルを考える。

カラム
id bigint
name string
email string
encrypted_password string
age integer
bio text(自己紹介などの長文)
settings json(設定の入れ子データ)
created_at / updated_at datetime

📊 DB の行サイズ ≠ Ruby のメモリ消費

User.all 自体は ActiveRecord::Relation を返すだけだが(詳しくはセクション2で後述)、これを列挙すると全カラムを 1 行ごとに読み込み、各行をモデルインスタンスへ変換する。このとき注意したいのは、DB 上の行のバイト数と、Ruby オブジェクトとして消費するメモリは別物だという点。1 行のロードで生成されるのは 1 オブジェクトではなく、行ごとの属性管理に加えて列ごとの属性オブジェクトが伴うため、Ruby 側のメモリは DB の行サイズより大きくなりやすい。


📊 件数ごとのメモリ消費の目安

フルのモデルインスタンス 1 件あたりのメモリを、短い列だけなら約 2KB、長い bio/settings を含むと平均約 12KB と仮定すると、件数に応じておおよそ次の規模になる。

件数 短い列のみ(≒2KB/件) 長い text/json 含む(≒12KB/件)
1,000 件 約 2MB 約 12MB
10,000 件 約 20MB 約 120MB
100,000 件 約 200MB 約 1.2GB

長い text/json 列を含むと、10 万件で 1GB を超える規模になり得る。json 列は Rails 側で Hash/Array にデシリアライズされると、元の JSON 文字列よりさらにメモリ上で膨らむこともある。

この数値はあくまで「ロード時に Ruby 側で確保される量のざっくりした桁感」で、memory_profiler の allocated・Ruby のヒープ・実プロセスの RSS 増加量と一致するものではない。実際の量は Ruby/Rails のバージョン、列数、値の長さ、TEXT/JSON の有無、GC(ガベージコレクション。不要になったメモリを自動回収する仕組み)の状態に大きく左右される。正確に知りたい場合は memory_profiler などで計測する。


💡 補足: 「全カラム取得」は ActiveRecord 固有ではない

「全カラムを取得してしまう」のは ActiveRecord 固有の欠点ではなく、行をモデル/結果オブジェクトにマップする ORM(Object-Relational Mapping。DB のレコードとプログラムのオブジェクトを対応づける仕組み)では、デフォルトが全(スカラー)カラムになりがちだ。何を取得するかをコード上どう指定するかが言語・ライブラリで異なる。

// Go の手書き SQL(sqlx。database/sql の薄い拡張)
// SQL を自分で書くので取得カラムは書いた通り。「デフォルト」という概念が無い
var users []User
db.Select(&users, "SELECT id, name FROM users") // id, name のみ
# Django(Python)
User.objects.all()                # 全カラム + モデルインスタンス
User.objects.values("id", "name") # id, name のみ(dict の QuerySet)
# only / defer はモデルインスタンスを維持したまま部分取得(未取得フィールド参照で追加クエリ)

2️⃣ オブジェクトがメモリに展開されるタイミング(遅延評価)

ActiveRecord::Relation は遅延評価される。whereorder などをつなげただけでは SQL は発行されない。

relation = User.where(active: true).order(:id)  # ここではまだ SQL を発行しない

実際に SQL が発行されるタイミングは、おおまかに次の通り。

  • 🔵 SQL を発行し、モデルオブジェクトを生成する: to_a / each / map などで列挙したとき、load / first / last / find など
  • 🟢 SQL は発行するが、モデルオブジェクトは生成しない: pluck / ids / count / sum などの集計、exists?

🔄 大量データにはバッチ処理

大量データを扱うときは、全件を一度にメモリへ展開する all.each ではなく、バッチ処理を使うとメモリ消費を抑えられる。

User.find_each(batch_size: 1000) do |user|
  # 1000 件ずつ読み込んで処理する
end

find_each / find_in_batches / in_batches はデフォルトで 1000 件ずつ処理する。


🔁 バッチ処理3メソッドの違い: ブロックに渡されるものが異なる

3つともデフォルトで 1000 件ずつ取得する点は同じだが、ブロックに渡される単位が異なる。

  • 1️⃣ find_each: レコードを 1 件ずつ 渡す。1 件ごとの処理を書きたいときに使う。
  • 📚 find_in_batches: レコードの 配列(バッチ単位) を渡す。バッチ単位でまとめて処理したいときに使う。
  • 🔗 in_batches: バッチを ActiveRecord::Relation として渡す。デフォルトで load: false(モデルインスタンス化しない)のため、update_all / delete_all のようなバッチ単位の一括操作に向く。

バッチサイズを指定するオプション名も異なり、find_each / find_in_batchesbatch_size:in_batchesof: を使う(いずれもデフォルト 1000)。

User.find_each { |user| puts user.name }            # user は 1 レコード
User.find_in_batches { |users| puts users.size }    # users は配列(最大 1000 件)
User.in_batches(of: 1000) { |relation| relation.update_all(active: true) }  # relation は Relation

3️⃣ パフォーマンスで気を付ける MySQL データ型

SELECT * で全カラムを取得する性質上、サイズの大きいカラムはメモリ・転送量の両面で負担になる。特に次の型に注意したい。

  • 📄 TEXT / BLOBMEDIUMTEXT / LONGTEXT 含む)
  • 📏 大きい VARCHAR
  • 🧩 JSON

MySQL の標準的な保存方式では、データを一定サイズの固まり(ページ)単位で扱う。TEXT や大きい VARCHAR のように長くなりがちなカラムは、行の本体とは別の場所に分けて保存されることがある。その場合、SELECT * でそれらまで取得すると、本体とは別の読み込みが追加で発生し得る。また JSON は読み込み時に Ruby のオブジェクトへ変換(デシリアライズ)するコストもかかる。


🛡️ 対策

対策としては、まず select / pluck で必要なカラムだけを取得するのが基本。

ignored_columns でモデル単位にカラムを SELECT から除外する方法もあるが、本来はカラム削除前の段階的移行のための機能で、属性アクセサも消えるため、性能目的の常用は避けるのが無難だ。

設計段階では、整数型は収まる範囲で最小の型(TINYINT / SMALLINT / INT / BIGINT)を選ぶ、VARCHAR の桁数を過大にしない(ソートや一時テーブルで宣言長分のメモリを確保しうる)といった点も効いてくる。


✅ まとめ

  • 🗂️ ActiveRecord はデフォルトで SELECT * を発行し全カラムをオブジェクト化するため、必要なカラムだけを select / pluck で取得するとメモリを抑えられる
  • Relation は遅延評価され、列挙や find などのタイミングで SQL 発行とオブジェクト生成が起きる。大量データはバッチ処理で扱う
  • 🧬 TEXT / BLOB / 大きい VARCHAR / JSON は読み込みコストが大きい。取得カラムの絞り込みと適切な型選択が効く

📚 参考文献

この記事は以下の情報を参考にして執筆しました。

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?