negishiです。
みなさんは、Railsでアプリケーションの機能やバッチ処理でデータをまとめてinsertする場合どうしていますか?
選択肢としては
- activerecord-importを使う。
- 標準のinsert_allを使う。(Rails 6.0.2.1~)
の2択になると思いますが、できる事が微妙に異なるのでざっくりまとめました。
TL;DR
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!にすれば例外発生します。