Rails
PostgreSQL

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

More than 3 years have passed since last update.


はじめに

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 やバリデーションなどが必要な場合についても要検討

以上、暇つぶしおわり。