Rails
PostgreSQL

暇つぶしに Rails アプリでテーブル全行フェッチする実装で遊んでみる

More than 3 years have passed since last update.


はじめに

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 を使ってみた


    • 地道に LIMITselect_rows でフェッチしても良い気はする

    • 最初、CSV ファイルに書き出してからパースという無駄な実装をしてしまい、しかも相当遅い処理になった


    • pgcopy_data のデコーダーを使って Array で受け取る方法を知った




  • COPY で1行ずつ Array で受け取る方法はそれなりに速い

  • 探せば一般的でより良い方法がある気がしてる

以上、暇つぶしおわり。