@ledsun blog

Hのキーがhellで、Sのキーがslaveだ、と彼は思った。そしてYのキーがyouだ。

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 } }

      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は早いです。

GitHub - zdennis/activerecord-import: A library for bulk insertion of data into your database using ActiveRecord.

This is the fastest import mechanism and also the most primitive.

と、あるだけのことはあります

バリデーションをスキップする

activerecord-importはバリデーションをスキップできます。

insert_all | Railsドキュメント

直接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側の性能に関しては、バッチサイズを変更する意味はなさそうです。