UUUMエンジニアブログ

UUUMのエンジニアによる技術ブログです

Railsのbulk insert事情

negishiです。
みなさんは、Railsでアプリケーションの機能やバッチ処理でデータをまとめてinsertする場合どうしていますか?

選択肢としては

の2択になると思いますが、できる事が微妙に異なるのでざっくりまとめました。

TL;DR

*1

insert_all activerecord-import
実行速度
batch size指定 ×
自動timestamp ○ (ver. 7 ~)*2
arg:hash
arg:model objects ×
association ○ (ver. 6.1 ~)*3
validation ×
callback ×

特に理由がない限り*4activerecord-import使いましょう。(執筆時点 Rails ~ 7.0.4)

実行速度

下記コードでベンチマークしました。*5

require 'benchmark'

class Users < ApplicationRecord
  def self.bulk_insert_benchmark
    hash_data = 10_000.times.each_with_object([]) do |_, users|
      users << {name: 'hoge太郎', email: 'hoge@example.com'}
    end

    model_data = 10_000.times.each_with_object([]) do |_, users|
      users << User.new(name: 'hoge太郎', email: 'hoge@example.com')
    end

    Benchmark.bm 30 do |r|
      r.report "insert_all" do
        ActiveRecord::Base.transaction do
          User.insert_all hash_data, record_timestamps: true
          raise ActiveRecord::Rollback
        end
      end

      r.report "import hash + columns" do
        ActiveRecord::Base.transaction do
          columns = %i(name email)
          User.import columns, hash_data
          raise ActiveRecord::Rollback
        end
      end

      # この場合、配列内のhashが一貫している必要があるので注意してください。
      # https://github.com/zdennis/activerecord-import/issues/507
      r.report "import hash" do
        ActiveRecord::Base.transaction do
          User.import hash_data
          raise ActiveRecord::Rollback
        end
      end

      r.report "import model objects" do
        ActiveRecord::Base.transaction do
          User.import model_data
          raise ActiveRecord::Rollback
        end
      end
    end
  end
end

それぞれの実行速度は下記です。*6
なお、transactionが計測範囲に入ってしまっています。(再計測しないという強い意志)

user system total real
insert_all 0.775897 0.010493 0.786390 ( 1.673050)
import hash + columns 1.835435 0.021656 1.857091 ( 2.724070)
import hash 2.055377 0.150236 2.205613 ( 2.610810)
import model objects 2.807487 0.033700 2.841187 ( 3.461411)

insert_allを使う上での注意点

  • batch sizeの指定が現行のversion(~ 7.0.4)ではできません!(痛い)
  • created_atやupdated_atのtimestampを自動で入れられるのはRails7系からなので、Rails6系の場合は明示的にhashに含める必要があります。
  • 重複キーの場合、そのレコードはスキップされます。*7
  • 直接SQLを実行する為、validationやcallbackは使えません。

所感

insert_allの実行速度が一番早いですが、これまでの内容を踏まえるとまだ使用する場面はかなり限られるかな...といった感じです。 しばらくは引き続きactiverecord-importのお世話になりそうです。

*1:activerecord-importでできて、insert_allでできない基準で書いてます。

*2:https://github.com/rails/rails/pull/43003

*3:https://github.com/rails/rails/pull/38899

*4:どうしても"Railsで"、かつbatch sizeを気にしなくてよく、とにかく早くinsertしたい!とかでなければ

*5:本当は1000万件↑で計測したかったですが、メモリーやらMySQL側のpacket size等々考慮するのがめんどくさいので妥協しました。

*6:各10回試行した結果の平均を算出。見やすさ重視で当該コードは省略しています。

*7:insert_all!にすれば例外発生します。