Log目次

【Rails】ActiveRecordオブジェクトを生成するとどれくらいメモリを消費するのか検証してみた

作成日 2026-07-20更新日 2026-07-20

はじめに

Railsで大量データを扱う際、eachでActiveRecordオブジェクトを1件ずつ生成しながら処理するか、pluckで必要なカラムだけ生の値として取得するかという選択肢があります。

「pluckの方が速そう」というイメージはあったものの、実際どれくらい差が出るのかを把握していなかったので、10万件のレコードを使って検証してみました。

実行環境


検証コード

まず、計測用の共通処理をModuleにまとめました。

module Test::Measurable def largest_tenant_id Employee.group(:tenant_id).count.max_by { |_, count| count }.first end def measure(label) GC.start before_rss = rss_mb before_objects = GC.stat[:total_allocated_objects] started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) yield time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at after_objects = GC.stat[:total_allocated_objects] after_rss = rss_mb { label: label, time: time, rss_before: before_rss, rss_after: after_rss, rss_diff: after_rss - before_rss, allocated_objects: after_objects - before_objects, } end def report(result) puts "=" * 70 puts result[:label] puts format(" 処理時間 : %.3f 秒", result[:time]) puts format(" RSS : %.1f MB -> %.1f MB(差分 %.1f MB)", result[:rss_before], result[:rss_after], result[:rss_diff]) puts format(" 生成オブジェクト数 : %d", result[:allocated_objects]) puts "=" * 70 end def rss_mb if File.exist?("/proc/self/status") kb = File.read("/proc/self/status")[/VmRSS:\s*(\d+) kB/, 1].to_i kb / 1024.0 else `ps -o rss= -p #{Process.pid}`.to_i / 1024.0 end end end

計測前にGC.startでガベージコレクションを実行し、GC.stat[:total_allocated_objects]とRSS(実際に使用している物理メモリ量)の差分を計測することで、処理によって新たに生成されたオブジェクト数とメモリ増加量を可視化しています。


ActiveRecordオブジェクトを生成するパターン

class Test::WithActiveRecord include Test::Measurable def call(tenant_id: nil) tenant_id ||= largest_tenant_id total = Employee.where(tenant_id: tenant_id).count puts "対象テナントID: #{tenant_id}#{total}件)" result = measure("ActiveRecordオブジェクトを生成する場合") { run(tenant_id) } report(result) result end private def run(tenant_id) count = 0 Employee.where(tenant_id: tenant_id).each do |employee| row = { id: employee.id, staff_code: employee.staff_code, name: "#{employee.last_name} #{employee.first_name}", email: employee.email, } count += 1 end count end end

pluckで生の値だけ取得するパターン

class Test::WithoutActiveRecord include Test::Measurable def call(tenant_id: nil) tenant_id ||= largest_tenant_id total = Employee.where(tenant_id: tenant_id).count puts "対象テナントID: #{tenant_id}#{total}件)" result = measure("ActiveRecordオブジェクトを生成しない場合(pluck)") { run(tenant_id) } report(result) result end private def run(tenant_id) count = 0 Employee.where(tenant_id: tenant_id).pluck(:id, :staff_code, :last_name, :first_name, :email).each do |id, staff_code, last_name, first_name, email| row = { id: id, staff_code: staff_code, name: "#{last_name} #{first_name}", email: email } count += 1 end count end end

どちらも取得したレコードから同じ形のHash(row)を作るだけの処理ですが、片方はActiveRecordオブジェクトを経由し、もう片方はpluckで必要なカラムだけを配列として取得しています。


実行

ActiveRecordオブジェクトを生成する場合

対象テナントID: 4(100000件)
======================================================================
ActiveRecordオブジェクトを生成する場合
  処理時間           : 1.869 秒
  RSS                : 98.0 MB -> 313.1 MB(差分 215.1 MB)
  生成オブジェクト数 : 2207060
======================================================================

pluckで生の値だけ取得する場合

対象テナントID: 4(100000件)
======================================================================
ActiveRecordオブジェクトを生成しない場合(pluck)
  処理時間           : 0.718 秒
  RSS                : 99.2 MB -> 188.4 MB(差分 89.2 MB)
  生成オブジェクト数 : 1300492
======================================================================

結果

10万件を一括ロードした場合の比較結果です。

比較項目AR生成ありAR生成なし(pluck)
処理時間1.869秒0.718秒
RSS差分215.1MB89.2MB
生成オブジェクト数2,207,0601,300,492

find_eachのような分割取得を使わず、全レコードを一度にメモリ上に保持する条件で比較したことで、両者の差がより明確に出る結果になりました。


まとめ