ファーストビュー画像
ヘッダーロゴ
ホームアイコン
>
>
【Rails】大量データを扱う処理を最適化する
バックエンド

【Rails】大量データを扱う処理を最適化する

作成日2025/05/31
更新日2025/06/04
アイキャッチ
# Ruby on Rails

今回はRailsで大量のデータを扱う場合の良い処理、悪い処理のパフォーマンスを比較していきます。

検証環境は以下です。

  • マシン:Apple MacBook Pro
  • チップ:Apple M3
  • メモリ:16GB
  • Ruby:3.3.4
  • Rails:8.0.2
  • DB:PostgreSQL 14.15 (Homebrew)
  • 実行環境:ローカル(Docker未使用)

Railsプロジェクトを作成済みの前提で進めていきます。

大容量CSVの読み込み

以下のような列を持つ3万行のCSVファイルを準備してpublicディレクトリ配下にusers.csvとして保存しておきます。

氏名,メールアドレス,電話番号
氏名1,gFa_xR4@test.jp,090-3473-1364
氏名2,G6IJzgAH19@sample.co.jp,090-6704-2237
氏名3,Ru6kp_X8ww@sample.org,090-2477-5567
...

readを使う場合

以下のようにCSV.read メソッドを使用すると、CSVファイル全体を一度に読み込むことができますが、大規模なファイルに対しては効率が低下します。

csv = CSV.read("public/users.csv", headers: true)

csv.each do |row|
  # 処理
end

foreachを使う場合

以下のようにCSV.foreachメソッドを使用するとCSVファイルを1行ずつ読み込むため、ファイルサイズが大きくなっても安定した効率を維持できます。

CSV.foreach("public/users.csv", headers: true) do |row|
  # 処理
end

比較

パフォーマンス測定用のモジュールであるBenchmarkを使ってCSV.readCSV.foreachそれぞれを使った場合の比較を行います。

namespace :csv do
  task read: :environment do
    Benchmark.bm do |x|
      x.report("read") do
        csv = CSV.read("public/users.csv", headers: true)

        csv.each do |row|
          # 処理
        end
      end

      x.report("foreach") do
        CSV.foreach("public/users.csv", headers: true) do |row|
          # 処理
        end
      end
    end
  end
end

実行結果

$ rake csv:read
             user     system      total        real
read     0.107221   0.002387   0.109608 (  0.110811)
foreach  0.074999   0.000900   0.075899 (  0.076095)

3列/3万行のCSVファイルを読み取る場合では、CSV.foreachを使う方がCSV.readを使う場合に比べて若干早いかなという感じでした。

今回は速度よりもメモリの使用量を計測する方が適切かもしれません。

大量のデータを保存

先ほどのCSVの内容でデータを保存するためにUserモデルを作成します。

マイグレーションファイル

class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :phone

      t.timestamps
    end

    add_index :users, :email, unique: true
  end
end

モデルファイル

class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
end

行数分insertする場合

この場合、以下のように1行ずつデータを保存する処理にしてしまうと毎ループごとにDBへのinsert処理が走ってしまい、効率が悪いです。

CSV.foreach("public/users_1000.csv", headers: true) do |row|
  name = row["氏名"]
  email = row["メールアドレス"]
  phone = row["電話番号"]
  user = User.new(name: name, email: email, phone: phone)
  user.save
end

一括でinsertする場合

ループ内ではDBにアクセスする処理はせず、データを配列にまとめる処理のみ行い、最後にinsert_allを用いて一括で処理することで大幅に効率をあげることができます。

user_attributes = []
CSV.foreach("public/users_1000.csv", headers: true) do |row|
  user_attributes << {
    name: row["氏名"],
    email: row["メールアドレス"],
    phone: row["電話番号"]
  }
end

User.insert_all(user_attributes)

比較

毎ループごとにinsertする場合と一括でinsertする場合を比較します。

namespace :users do
  task import: :environment do
    Benchmark.bm do |x|
      User.delete_all
      x.report("User.create") do
        CSV.foreach("public/users.csv", headers: true) do |row|
          name = row["氏名"]
          email = row["メールアドレス"]
          phone = row["電話番号"]
          user = User.new(name: name, email: email, phone: phone)
          user.save
        end
      end

      User.delete_all
      x.report("User.insert_all") do
        user_attributes = []
        CSV.foreach("public/users.csv", headers: true) do |row|
          user_attributes << {
            name: row["氏名"],
            email: row["メールアドレス"],
            phone: row["電話番号"]
          }
        end

        User.insert_all(user_attributes)
      end
    end
  end
end

実行結果

$ rake users:import
                     user     system      total        real
User.create     31.516866   1.615482  33.132348 ( 44.601783)
User.insert_all  0.437329   0.011089   0.448418 (  0.812219)

3列/3万行のデータを保存する場合は、一括でinsertをする方が毎ループごとにinsertする場合に比べて約55倍高速でした。

insert_allを分割した方が良いパターン

insert_allを使って一括でinsertをする場合でも行数や列数がさらに多い場合は分割してinsert_allを実行した方が効率が良いこともあります。

user_attributes = []
CSV.foreach("public/users.csv", headers: true) do |row|
  user_attributes << {
    name: row["氏名"],
    email: row["メールアドレス"],
    phone: row["電話番号"]
  }
end

user_attributes.each_slice(1000) do |slice|
  User.insert_all(slice)
end

以下のように列数を13、行数を90000件まで増やしたCSVファイルで試してみます。
Userモデルのカラムも修正、追加をしています。

性,名,フリガナ(性),フリガナ(名),メールアドレス,電話番号,性別,生年月日,年齢,郵便番号,都道府県,都道府県コード,IPアドレス
性,名,セイ,メイ,KZFeDKsBNF@example.org,090-7451-3388,男,1968/3/2,57,914-4177,埼玉県,11,128.238.9.109
性,名,セイ,メイ,K4xgmAxk@sample.net,080-3039-2473,男,1966/11/29,58,823-4155,三重県,24,85.195.29.76
...
require "csv"
require "benchmark"

namespace :users_13_90000 do
  task import: :environment do
    Benchmark.bm do |x|
      User.delete_all
      x.report("User.insert_all") do
        user_attributes = []
        CSV.foreach("public/users_13_90000.csv", headers: true) do |row|
          user_attributes << {
            name_sei: row["性"],
            name_mei: row["名"],
            name_sei_kana: row["フリガナ(性)"],
            name_mei_kana: row["フリガナ(名)"],
            email: row["メールアドレス"],
            phone: row["電話番号"],
            gender: row["性別"],
            birthdate: row["生年月日"],
            age: row["年齢"],
            zipcode: row["郵便番号"],
            prefecture: row["都道府県"],
            prefecture_code: row["都道府県コード"],
            ip_address: row["IPアドレス"]
          }
        end

        User.insert_all(user_attributes)
      end

      User.delete_all
      x.report("User.insert_all.each_slice") do
        user_attributes = []
        CSV.foreach("public/users_13_90000.csv", headers: true) do |row|
          user_attributes << {
            name_sei: row["性"],
            name_mei: row["名"],
            name_sei_kana: row["フリガナ(性)"],
            name_mei_kana: row["フリガナ(名)"],
            email: row["メールアドレス"],
            phone: row["電話番号"],
            gender: row["性別"],
            birthdate: row["生年月日"],
            age: row["年齢"],
            zipcode: row["郵便番号"],
            prefecture: row["都道府県"],
            prefecture_code: row["都道府県コード"],
            ip_address: row["IPアドレス"]
          }
        end

        user_attributes.each_slice(1000) do |slice|
          User.insert_all(slice)
        end
      end
    end
  end
end

実行結果

$ rake users_13_90000:import
                                user     system      total        real
User.insert_all             4.403736   0.112571   4.516307 (  6.142268)
User.insert_all.each_slice  3.759315   0.074634   3.833949 (  5.744757)

劇的ではありませんが、若干改善されています。
この辺りは、カラム数、レコード数を見て分割数を調整していく必要がありそうです。

大量のデータを削除

13列/3万行のCSVから作成したUserのレコードを全て削除する場合を見ていきます。

destroy_allを使用する場合

以下のようにdestroy_allを使用すると対象レコードを1件ずつeachで取り出し、それぞれに対してdestroyを実行するため、データ量が多い場合には効率が低下します。

User.destroy_all

delete_allを使用する場合

delete_allでは各レコードを呼び出すことなく単一のSQL文で処理が完了するため、大量レコードの場合はdestroy_allに比べて大幅に効率が向上します。

User.delete_all

比較

destroy_alldelete_allのそれぞれを使った場合を比較します。

require "csv"
require "benchmark"

namespace :users do
  task destroy: :environment do
    Benchmark.bm do |x|
      insert_users
      x.report("User.destroy_all") do
        User.destroy_all
      end

      insert_users
      x.report("User.delete_all") do
        User.delete_all
      end
    end
  end
end

def insert_users
  user_attributes = []
  CSV.foreach("public/users_13_30000.csv", headers: true) do |row|
    user_attributes << {
      name_sei: row["性"],
      name_mei: row["名"],
      name_sei_kana: row["フリガナ(性)"],
      name_mei_kana: row["フリガナ(名)"],
      email: row["メールアドレス"],
      phone: row["電話番号"],
      gender: row["性別"],
      birthdate: row["生年月日"],
      age: row["年齢"],
      zipcode: row["郵便番号"],
      prefecture: row["都道府県"],
      prefecture_code: row["都道府県コード"],
      ip_address: row["IPアドレス"]
    }
  end

  User.insert_all(user_attributes)
end

実行結果

                      user     system      total        real
User.destroy_all 20.248357   1.273361  21.521718 ( 28.356763)
User.delete_all   0.000409   0.000060   0.000469 (  0.010724)

13カラムのレコードを3万件削除する場合、delete_allを使った方が、destroy_allを使った時に比べて大幅に高速でした。

注意点

仮に以下のようにPostモデルとの関連付けを定義していた場合、User.delete_allを実行してもPostレコードは削除されません。また、コールバックもdelete_allでは実行されないので注意が必要です。

has_many :posts, dependent: :destroy

もし上記のような関連付けをしている場合は先にPostレコードを削除する必要があります。

Post.delete_all
User.delete_all

まとめ

それぞれのメソッドがどのような処理を行うのかを把握した上でレコード数、カラム数などを考慮して最適な処理を実装する必要があります。

また、バリデーションやコールバックの実行有無も把握した上で使用しないと思わぬ不具合に繋がる恐れもあるので注意が必要です。

share on
xアイコンfacebookアイコンlineアイコン