LoginSignup
2
3

More than 5 years have passed since last update.

暇つぶしに Rails アプリでテーブルにそこそこの行数を入れる実装で遊んでみる

Posted at

はじめに

BULK INSERT と COPY なら COPY の方が圧倒的強者なはず。これは、そんな些細な事が気になって暇つぶしがてらに遊んでみた記録。

制限

  • PostgreSQL の事しか考えない
  • 空のテーブルに新規データ登録する事だけを考える
  • ユニーク制約についても考えない(衝突しない)
  • 遊びなので全体的に雑

activerecord-import について

BULK INSERT の実装として zdennis/activerecord-import を使う。自分で BULK INSERT 書くのが面倒なので。

  • バリデーションしてくれるけど、インスタンス化するからひどく遅くなる
  • 成功行数とかとれるけど、必要ない場合を考える
  • MySQL だったら INSERT ... ON DUPLICATE KEY UPDATE に対応してて上書きとかできるけど、これも必要ない場合を考える

環境

  • MacBookAir7,1
  • Intel Core i7 2.2 GHz
  • DDR3 4GBx2
  • APPLE SSD SD0256F
  • Mac OS X 10.11.4
  • Ruby 2.3.0p0 (rbenv)
    • Rails 4.2.6
    • pg 0.18.4
    • postgres-copy 1.0.0
    • activerecord-import 0.13.0
    • ridgepole 0.6.4
  • PostgreSQL 9.5.2

準備

テーブル

Schemafile
create_table 'hundred_columns', force: :cascade do |t|
  100.times do |n|
    t.string "column_#{n}"
  end
end

モデル

$ rails g model HundredColumn
app/models/hundred_column.rb
class HundredColumn < ActiveRecord::Base
  acts_as_copy_target
end

計測

require 'benchmark'
require 'csv'
require 'hundred_column'

Tempfile.create('benchmark-import') do |tmp|
  row = Array.new(100) { |i| "column #{i}" }.join(',')
  10_000.times { tmp.puts row }
  tmp.flush

  Benchmark.bm(35) do |x|
    header = Array.new(100) { |i| "column_#{i}" }

    HundredColumn.connection.execute('TRUNCATE TABLE hundred_columns')
    x.report('activerecord-import/validate: true') do
      csv = CSV.foreach(tmp.path)
      csv.each_slice(500) do |rows|
        HundredColumn.import header, rows
      end
    end

    HundredColumn.connection.execute('TRUNCATE TABLE hundred_columns')
    x.report('activerecord-import/validate: false') do
      csv = CSV.foreach(tmp.path)
      csv.each_slice(500) do |rows|
        HundredColumn.import header, rows, validate: false
      end
    end

    HundredColumn.connection.execute('TRUNCATE TABLE hundred_columns')
    x.report('postgres-copy copy_from') do
      HundredColumn.copy_from tmp.path, header: nil, columns: header
    end

    HundredColumn.connection.execute('TRUNCATE TABLE hundred_columns')
    conn = HundredColumn.connection.raw_connection
    enco = PG::TextEncoder::CopyRow.new
    x.report('pg copy_data') do
      conn.copy_data "COPY hundred_columns (#{header.join(',')}) FROM STDIN", enco do
        CSV.foreach(tmp.path) do |row|
          conn.put_copy_data row
        end
      end
    end
  end
end

__END__
                                          user     system      total        real
activerecord-import/validate: true   21.260000   0.270000  21.530000 ( 23.511967)
activerecord-import/validate: false   6.270000   0.080000   6.350000 (  7.988115)
postgres-copy copy_from               0.050000   0.020000   0.070000 (  0.270931)
pg copy_data                          0.880000   0.030000   0.910000 (  1.078610)

まとめ

  • activerecord-import は遅い
  • COPY は優秀
  • 途中で問題がある行に遭遇した時、COPY は全部なかったことになる
  • UPSERT やバリデーションなどが必要な場合についても要検討

以上、暇つぶしおわり。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3