Rails bulk insertのパフォーマンス比較 (Railsのinsert_allとactiverecord-import)

Railsでbulk insertする方法は主に

の2つの方法があります。パフォーマンス観点では、どちらが良いのか知りたくて検証してみました。

activerecord-importに関しては、READMEを読む限り、いくつかimportの方法がありますが、Rails6のinsert_allと同様にバリデーションを行わない以下2つの方法で比較しました。

  • Works with raw columns and arrays of values (fastest)
  • Works with model objects (faster)
    • => モデルオブジェクトを使う方法

結論

結論としては、activerecord-importのカラム名と値の配列を使う方法が一番早かったです。

以下は10万件のレコードをbulk insertした結果なのですが、件数を変えたり、何回か実行してみても、大体同じような結果になりました。レコード件数を変えて、複数回実行して平均を取った結果を以下の比較図に記載しています。

[1] pry(main)> Book.bulk_insert
                          user        system    total      real
Rails6 insert_all       13.024230   0.051869  13.076099 ( 14.523158)
import (カラム名と値の配列) 11.924948   0.048174  11.973122 ( 13.471458)
import (モデルオブジェクト) 20.419098   0.155907  20.575005 ( 22.313445)

環境

検証環境のスペックは以下の通りです。

  • OS: Ubuntu 18.04.2 LTS
  • PC: メモリ 15.4 G、CPU Itel Core i7-8650U(1.90GHz [コア/スレッド数 4/8])
  • MySQL: 5.7.29 (InnoDB)

検証方法

以下のコードを実行しました。

require 'benchmark'

class Book < ActiveRecord::Base
  class << self
    def bulk_insert
      # Rails6で導入された insert_all で bulk insert するデータ
      insert_data =
        100_000.times.each_with_object([]) do |_, array|
          array << { name: 'book name', author: 'book auther', created_at: Time.current, updated_at: Time.current }
        end

      # activerecord-import の import(カラム名と値の配列) で bulk insert するデータ
      import_data = [['book name', 'book auther', Time.current, Time.current]] * 100_000

      # activerecord-import の import(モデルオブジェクト) で bulk insert するデータ
      import_model_data =
        100_000.times.each_with_object([]) do |_, array|
          array << new(name: 'book name', author: 'book auther', created_at: Time.current, updated_at: Time.current)
        end

      Benchmark.bm 30 do |r|
        # Rails6で導入された insert_all で bulk insert
        transaction do
          r.report 'insert_all' do
            insert_all insert_data
          end
          raise ActiveRecord::Rollback
        end 

        # activerecord-import の import(カラム名と値の配列で bulk insert
        transaction do
          r.report 'import (カラム名と値の配列)' do
            columns = [:name, :author, :created_at, :updated_at]
            import columns, import_data, validate: false
          end
          raise ActiveRecord::Rollback
        end 

        # activerecord-import の import(モデルオブジェクト) で bulk insert
        transaction do
          r.report 'import (モデルオブジェクト)' do
            import import_model_data, validate: false
          end
          raise ActiveRecord::Rollback
        end 
      end
    end
  end
end

比較図

レコード10万件から1桁ずつ減らして、パフォーマンスを計測して図(値はBenchmarkのReal)にしてみました。Rails6 insert_allは、activerecord-import の import (カラム名と値の配列) よりちょっと遅いくらいだということがわかります。 f:id:moritamorie:20200327231420p:plain

感想

2020年3月時点では、Rails 6のinsert_allはパフォーマンス面ではactiverecord-importほどではなく、バッチサイズを指定できなかったり、モデルのバリデーションやコールバックが実行できないので、もう少し機能が増えてきたら用途が増えそうという感じがしています。

関わっている案件ではactiverecord-import使っているけど、置き換えるのはまだちょっと先かなと思ってます.