速度を計測する簡単なスクリプトが手に入りました。 計測しやすくするために、よりシンプルに変更します。
class User < ApplicationRecord class << self def benchmark_bulk_insert # create data instances = [] 1_000.times { instances << new(name: 'name', created_at: Time.current, updated_at: Time.current) } hashes = [] 1_000.times { hashes << { name: 'name', created_at: Time.current, updated_at: Time.current } } values = [] 1_000.times { values << "('name', '#{Time.current.to_s(:db)}', '#{Time.current.to_s(:db)}')" } sql = "INSERT INTO users (name, created_at, updated_at) VALUES #{values.join(',')}" Benchmark.bm 40 do |r| run(r, 'sql') { connection.execute sql } run(r, 'insert_all') { insert_all hashes } run(r, 'import instances') { import instances } end end def run(r, message) transaction do r.report(message) { 10.times { yield } } raise ActiveRecord::Rollback end end end end
実行します。
user system total real sql 0.011214 0.000122 0.011336 ( 0.011334) insert_all 0.433187 0.000000 0.433187 ( 0.433194) import instances 0.397242 0.000000 0.397242 ( 0.397241)
データの形式を変える
activerecord-importは、入力データの形式にActiveRecordインスタンスのほかに、次の形式が選べます。
- ハッシュ
- Columns And Arrays
試してみましょう。
class User < ApplicationRecord class << self def benchmark_bulk_insert # create data instances = [] 1_000.times { instances << new(name: 'name', created_at: Time.current, updated_at: Time.current) } hashes = [] 1_000.times { hashes << { name: 'name', created_at: Time.current, updated_at: Time.current } } arrays = [] 1_000.times { arrays << ['name', Time.current.to_s(:db), Time.current.to_s(:db)] } Benchmark.bm 40 do |r| run(r, 'insert_all') { insert_all hashes } run(r, 'import instances') { import instances } run(r, 'import hashes') { import hashes } run(r, 'import columns and arrays') { import [:name, :created_at, :updated_at], arrays } end end def run(r, message) transaction do r.report(message) { 10.times { yield } } raise ActiveRecord::Rollback end end end end
user system total real insert_all 0.431001 0.000000 0.431001 ( 0.431001) import instances 0.446847 0.000000 0.446847 ( 0.446847) import hashes 0.541531 0.000000 0.541531 ( 0.541530) import columns and arrays 0.351939 0.000000 0.351939 ( 0.351939)
ハッシュは遅くなります。意外です。 Columns And Arraysは早いです。
This is the fastest import mechanism and also the most primitive.
と、あるだけのことはあります
バリデーションをスキップする
activerecord-importはバリデーションをスキップできます。
直接SQLを実行するのでバリデーションやコールバックはスキップ
insert_allはもともとスキップしています。 試してみましょう。
class User < ApplicationRecord class << self def benchmark_bulk_insert # create data instances = [] 1_000.times { instances << new(name: 'name', created_at: Time.current, updated_at: Time.current) } hashes = [] 1_000.times { hashes << { name: 'name', created_at: Time.current, updated_at: Time.current } } arrays = [] 1_000.times { arrays << ['name', Time.current.to_s(:db), Time.current.to_s(:db)] } Benchmark.bm 40 do |r| run(r, 'insert_all') { insert_all hashes } run(r, 'import instances') { import instances } run(r, 'import instances without validations') { import instances, validate: false } run(r, 'import hashes without validations') { import hashes, validate: false } run(r, 'import c and a without validations') { import [:name, :created_at, :updated_at], arrays, validate: false } end end def run(r, message) transaction do r.report(message) { 10.times { yield } } raise ActiveRecord::Rollback end end end end
user system total real insert_all 0.430299 0.000000 0.430299 ( 0.430296) import instances 0.404580 0.000000 0.404580 ( 0.404608) import instances without validations 0.330146 0.000000 0.330146 ( 0.330145) import hashes without validations 0.408283 0.000000 0.408283 ( 0.408279) import c and a without validations 0.203939 0.000000 0.203939 ( 0.203946)
元の倍ぐらい速くなりました。 ActiveRecord.insert_allを使っていて、SQLがボトルネックではなくSQL文字列生成がボトルネックの場合、activerecord-importに置き換えるのは有効そうです。
おまけ
バッチサイズをかえる
activerecord-importにはバルクインサートのバッチサイズを指定するオプションがあります。 試してみました。
class User < ApplicationRecord class << self def benchmark_bulk_insert # create data instances = [] 1_000.times { instances << new(name: 'name', created_at: Time.current, updated_at: Time.current) } hashes = [] 1_000.times { hashes << { name: 'name', created_at: Time.current, updated_at: Time.current } } arrays = [] 1_000.times { arrays << ['name', Time.current.to_s(:db), Time.current.to_s(:db)] } Benchmark.bm 40 do |r| run(r, 'insert_all') { insert_all hashes } run(r, 'import instances') { import instances } run(r, 'import instances batch_size 1') { import instances, batch_size: 1 } run(r, 'import instances batch_size 10') { import instances, batch_size: 10 } run(r, 'import instances batch_size 100') { import instances, batch_size: 100 } run(r, 'import instances batch_size 1000') { import instances, batch_size: 1000 } end end def run(r, message) transaction do r.report(message) { 10.times { yield } } raise ActiveRecord::Rollback end end end end
user system total real insert_all 0.439237 0.000146 0.439383 ( 0.439380) import instances 0.409890 0.000064 0.409954 ( 0.409953) import instances batch_size 1 1.376732 0.009987 1.386719 ( 1.386733) import instances batch_size 10 0.475238 0.009898 0.485136 ( 0.485136) import instances batch_size 100 0.380801 0.010013 0.390814 ( 0.390812) import instances batch_size 1000 0.376781 0.000000 0.376781 ( 0.376780)
バッチサイズを小さくすると遅くなります。 大きくすると多少速くなりますが、デフォルトと大差はありません。 Ruby側の性能に関しては、バッチサイズを変更する意味はなさそうです。