はじめに
find_each
はメモリに優しいけど、 ActiveRecord
のインスタンス化は時間に優しくない。これは、そんな些細な事が気になって暇つぶしがてらに遊んでみた記録。
制限
- PostgreSQL の事しか考えない
- フェッチしたデータを処理する際にモデルの機能が不要(インスタンス化不要)な場合の話
- 遊びなので全体的に雑
環境
- 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
- factory_girl 4.5.0
- ridgepole 0.6.4
- PostgreSQL 9.5.2
準備
テーブル
Schemafile
create_table 'ten_columns', force: :cascade do |t|
10.times do |n|
t.string "column_#{n}"
end
end
create_table 'hundred_columns', force: :cascade do |t|
100.times do |n|
t.string "column_#{n}"
end
end
モデル
$ rails g model TenColumn
$ rails g model HundredColumn
app/models/ten_column.rb
class TenColumn < ActiveRecord::Base
acts_as_copy_target
end
app/models/hundred_column.rb
class HundredColumn < ActiveRecord::Base
acts_as_copy_target
end
データ
1行あたりのデータ量は同じになる様に調整。
spec/factories/ten_columns.rb
FactoryGirl.define do
factory :ten_column do
10.times do |n|
sequence("column_#{n}") { 'a' * 100 }
end
end
end
spec/factories/hundred_columns.rb
FactoryGirl.define do
factory :hundred_column do
100.times do |n|
sequence("column_#{n}") { 'a' * 10 }
end
end
end
$ rails r 'FactoryGirl.create_list(:ten_column, 100000)'
$ rails r 'FactoryGirl.create_list(:hundred_column, 100000)'
計測
require 'ten_column'
require 'hundred_column'
require 'csv'
Benchmark.bm(16) do |x|
x.report('AR/10:10') do
TenColumn.find_each(batch_size: 500) { |_| }
end
10.step(100, 10) do |n|
columns = ['id']
n.times { |i| columns << "column_#{i}" }
x.report("AR/100:#{n}") do
HundredColumn.select(*columns).find_each(batch_size: 500) { |_| }
end
end
x.report('CSV/COPY/10:10') do
Tempfile.create('csv-10col') do |tmp|
TenColumn.copy_to tmp.path
CSV.foreach(tmp.path) { |_| }
end
end
10.step(100, 10) do |n|
columns = ['id']
n.times { |i| columns << "column_#{i}" }
x.report("CSV/COPY/100:#{n}") do
Tempfile.create("csv-100col-#{n}select") do |tmp|
HundredColumn.select(*columns).copy_to tmp.path
CSV.foreach(tmp.path) { |_| }
end
end
end
Tempfile.create('csv-10col') do |tmp|
TenColumn.copy_to tmp.path
x.report('CSV/10:10') do
CSV.foreach(tmp.path) { |_| }
end
end
10.step(100, 10) do |n|
columns = ['id']
n.times { |i| columns << "column_#{i}" }
Tempfile.create("csv-100col-#{n}select") do |tmp|
HundredColumn.select(*columns).copy_to tmp.path
x.report("CSV/100:#{n}") do
CSV.foreach(tmp.path) { |_| }
end
end
end
sql = TenColumn.all.to_sql
conn = TenColumn.connection.raw_connection
deco = PG::TextDecoder::CopyRow.new
x.report('COPY/10') do
conn.copy_data "COPY (#{sql}) TO STDOUT", deco do
while row = conn.get_copy_data
end
end
end
10.step(100, 10) do |n|
columns = ['id']
n.times { |i| columns << "column_#{i}" }
sql = HundredColumn.select(*columns).all.to_sql
conn = TenColumn.connection.raw_connection
deco = PG::TextDecoder::CopyRow.new
x.report("COPY/100:#{n}") do
conn.copy_data "COPY (#{sql}) TO STDOUT", deco do
while row = conn.get_copy_data
end
end
end
end
end
__END__
user system total real
AR/10:10 1.680000 0.190000 1.870000 ( 2.156565)
AR/100:10 1.450000 0.040000 1.490000 ( 1.672375)
AR/100:20 1.830000 0.050000 1.880000 ( 2.062489)
AR/100:30 2.390000 0.080000 2.470000 ( 2.695000)
AR/100:40 2.910000 0.080000 2.990000 ( 3.275444)
AR/100:50 3.370000 0.130000 3.500000 ( 3.822395)
AR/100:60 3.910000 0.120000 4.030000 ( 4.406621)
AR/100:70 4.550000 0.150000 4.700000 ( 5.203402)
AR/100:80 4.970000 0.150000 5.120000 ( 5.575259)
AR/100:90 5.290000 0.150000 5.440000 ( 5.904406)
AR/100:100 6.150000 0.180000 6.330000 ( 6.853067)
CSV/COPY/10:10 2.260000 0.070000 2.330000 ( 2.956577)
CSV/COPY/100:10 1.220000 0.020000 1.240000 ( 1.433307)
CSV/COPY/100:20 2.080000 0.020000 2.100000 ( 2.389060)
CSV/COPY/100:30 2.990000 0.030000 3.020000 ( 3.444904)
CSV/COPY/100:40 3.830000 0.040000 3.870000 ( 4.393019)
CSV/COPY/100:50 4.520000 0.050000 4.570000 ( 5.199091)
CSV/COPY/100:60 5.560000 0.070000 5.630000 ( 6.419458)
CSV/COPY/100:70 6.280000 0.110000 6.390000 ( 7.337055)
CSV/COPY/100:80 7.040000 0.120000 7.160000 ( 8.266461)
CSV/COPY/100:90 7.860000 0.150000 8.010000 ( 9.561194)
CSV/COPY/100:100 8.680000 0.180000 8.860000 ( 10.270413)
CSV/10:10 2.130000 0.070000 2.200000 ( 2.238094)
CSV/100:10 1.290000 0.030000 1.320000 ( 1.332233)
CSV/100:20 2.160000 0.040000 2.200000 ( 2.247108)
CSV/100:30 2.760000 0.040000 2.800000 ( 2.848656)
CSV/100:40 3.810000 0.060000 3.870000 ( 3.957855)
CSV/100:50 4.430000 0.060000 4.490000 ( 4.549781)
CSV/100:60 5.350000 0.100000 5.450000 ( 5.615255)
CSV/100:70 5.880000 0.080000 5.960000 ( 6.024053)
CSV/100:80 7.160000 0.160000 7.320000 ( 7.745105)
CSV/100:90 7.990000 0.170000 8.160000 ( 8.405227)
CSV/100:100 8.590000 0.140000 8.730000 ( 8.945959)
COPY/10 1.210000 0.130000 1.340000 ( 1.352775)
COPY/100:10 0.200000 0.040000 0.240000 ( 0.233236)
COPY/100:20 0.400000 0.050000 0.450000 ( 0.485219)
COPY/100:30 0.430000 0.050000 0.480000 ( 0.505082)
COPY/100:40 0.570000 0.070000 0.640000 ( 0.646616)
COPY/100:50 0.670000 0.070000 0.740000 ( 0.742535)
COPY/100:60 0.760000 0.070000 0.830000 ( 0.842084)
COPY/100:70 0.990000 0.080000 1.070000 ( 1.112195)
COPY/100:80 0.950000 0.080000 1.030000 ( 1.039029)
COPY/100:90 1.100000 0.100000 1.200000 ( 1.201436)
COPY/100:100 1.240000 0.100000 1.340000 ( 1.354664)
まとめ
-
ActiveRecord
のインスタンス化はカラム数に比例して遅くなる -
find_each
相当の事をActiveRecord
のインスタンス化なしに実現する目的で PostgreSQL のCOPY
を使ってみた- 地道に
LIMIT
とselect_rows
でフェッチしても良い気はする - 最初、CSV ファイルに書き出してからパースという無駄な実装をしてしまい、しかも相当遅い処理になった
-
pg
のcopy_data
のデコーダーを使ってArray
で受け取る方法を知った
- 地道に
-
COPY
で1行ずつArray
で受け取る方法はそれなりに速い - 探せば一般的でより良い方法がある気がしてる
以上、暇つぶしおわり。