LoginSignup
2
2

More than 5 years have passed since last update.

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

Posted at

はじめに

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 で受け取る方法はそれなりに速い
  • 探せば一般的でより良い方法がある気がしてる

以上、暇つぶしおわり。

2
2
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
2