約30万件、46MBのセミコロン区切りテキストをインポート
CSV ファイルを元にモデルにレコードをセットしたかったのですが、思った以上に処理が遅くてあれこれ試行錯誤したので備忘録的に。
Rails のお作法とかはガン無視のSQLベタ打ち実行なので、ほんまにこれで良いのかはちょっと疑問。
ちなみに取り込み対象のテキストファイルは以下の様な仕様。
- セミコロン区切り
- 文字コードはShift_JIS
- 約30万行
- 1行文字数151文字
- ファイルサイズ約46MB
レコードは全件洗い替えです。
あとバックエンドのRDBMSはMySQL5.6 で。*01PostgreSQL とかにもLOAD LOCAL DATA INFILE みたいなステートメントはあるんだろうか?
ActiveRecord でnew してsave だと20分かかる
まずは普通にActiveRecord のnew とsave で。
class MyModel < ActiveRecord::Base
require 'csv'
module CONSTANTS
ORIGINAL_FILE = 'tmp/my_data.txt'
SQL_RESET_SURROGATE_KEY = 'ALTER TABLE my_table AUTO_INCREMENT = 1;'
end # module CONSTANTS
CONSTANTS.freeze
def renew_all
MyModel.delete_all
MyModel.connection.execute(CONSTANTS::SQL_RESET_SURROGATE_KEY)
CSV.foreach(CONSTANTS::ORIGINAL_FILE, encoding: 'Shift_JIS:UTF-8', col_sep: ';') do |row|
record = MyModel.new
index = 0
record.hoge = row[index]
record.piyo = row[index += 1]
record.fuga = row[index += 1]
record.save
end # CSV.foreach
end # renew_all
end # class MyModel
これがバカみたいに時間がかかる。
30万行46MB の処理に約20分を要した。
gem mysql2-cs-bind でSQL をprepare 実行だと10分でいける
あまりに酷いので、ActiveRecord を介さずgem ‘mysql2-cs-bind’ を使って、prepare ステートメント実行してみた。
class MyModel < ActiveRecord::Base
require 'csv'
require 'mysql2-cs-bind'
module CONSTANTS
ORIGINAL_FILE = 'tmp/my_data.txt'
SQL_RESET_SURROGATE_KEY = 'ALTER TABLE my_table AUTO_INCREMENT = 1;'
SQL_INSERT_NEW_RECORD = 'INSERT INTO my_table (hoge, piyo, fuga) VALUES (?, ?, ?);'
end # module CONSTANTS
CONSTANTS.freeze
@client = Mysql2::Client.new(
:host => 'db_host',
:username => 'db_user',
:password => 'db_pass',
:database => 'db_name'
)
def renew_all
MyModel.delete_all
MyModel.connection.execute(CONSTANTS::SQL_RESET_SURROGATE_KEY)
CSV.foreach(original_file, encoding: 'Shift_JIS:UTF-8', col_sep: ';') do |row|
values = Array.new
index = 0
values.push( row[index] )
values.push( row[index += 1] )
values.push( row[index += 1] )
@client.xquery(CONSTANTS::SQL_INSERT_NEW_RECORD, values)
end # CSV.foreach
end # renew_all
end # class MyModel
これで10分。
でも、まだ遅い。
まだ遅い、ならばLOAD LOCAL DATA INFILE だ!
いっそMySQL に直接仕事させちゃおうという事で、LOAD LOCAL DATA INFILE ステートメントを実行。
class MyModel < ActiveRecord::Base
require 'csv'
require 'mysql2-cs-bind'
module CONSTANTS
ORIGINAL_FILE = 'tmp/my_file.txt'
SQL_RESET_SURROGATE_KEY = 'ALTER TABLE my_table AUTO_INCREMENT = 1;'
SQL_SET_CHARSET_SJIS = 'SET CHARACTER_SET_DATABASE = sjis;'
SQL_LOAD_LOCAL_DATA = "LOAD DATA LOCAL INFILE '#{ORIGINAL_FILE}'
INTO TABLE my_table FIELDS TERMINATED BY ';' (
hoge,
piyo,
fuga
);"
SQL_SET_CHARSET_UTF8 = 'SET CHARACTER_SET_DATABASE = utf8;'
end # module CONSTANTS
CONSTANTS.freeze
def renew_all
MyModel.delete_all
MyModel.connection.execute(CONSTANTS::SQL_RESET_SURROGATE_KEY)
MyModel.connection.execute(CONSTANTS::SQL_SET_CHARSET_SJIS)
MyModel.connection.execute(CONSTANTS::SQL_LOAD_LOCAL_DATA)
MyModel.connection.execute(CONSTANTS::SQL_SET_CHARSET_UTF8)
end # renew_all
end # class MyModel
なんと44秒!!
これだけの差がついてしまうと、ActiveModel がどうとか言っていられないですよなあ…。
(余談ですが、ActiveRecord のconnection を使わずMySQL直でLOAD LOCAL DATA INFILE を実行したら3秒でしたよ。)
あれー?
そんなわけで、Rails のModel にCSVを取り込む処理を書こうとしていたはずなのに、気が付けばMySQL に直接CSVを取り込ませる処理を書いていたっていうね。
ほんまはSQLベタ書きとかしたくないんですが、背に腹は代えられないです。
どなたか他に良い書き方を知っていましたら、ご教示頂けるとありがたい。
Also published on Medium.
脚注
| ↩01 | PostgreSQL とかにもLOAD LOCAL DATA INFILE みたいなステートメントはあるんだろうか? |
|---|