Rails アプリでまとまった数の DB のレコードをロードし、統計処理をして結果を DB に記録するということをしたところ、完了するまで10時間以上かかってしまいかなり遅いと感じた。 改善したいと思いコードを見直そうとしたのだが、これを R で実装したらかなり改善できるのではないかと思った。 既存のコードを R に移植するのはそこそこ時間がかかりそうだったので、簡略化した処理で実行時間の比較をし、
比較方法
- DB から 10 万件のレコードを読み、単純移動平均 (n = 25) を計算して DB に記録する
- 実行時間は time コマンドの結果とする
環境および実装
DB
テストデータのテーブル定義は以下
-- 統計対象 CREATE TABLE `test_data` ( `id` int(11) NOT NULL AUTO_INCREMENT, `value` float NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; -- 単純移動平均の結果 CREATE TABLE `smas` ( `id` int(11) NOT NULL AUTO_INCREMENT, `value` float NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;
Ruby
- Ruby 2.5.1
- ActiveRecord 5.2.1
- zdennis/activerecord-import 0.27.0
require "active_record" require "activerecord-import" class TestData < ActiveRecord::Base end class SMA < ActiveRecord::Base def self.calculate(test_data, n) 0.upto(test_data.size - 1 - n).map do |i| [test_data[i, n].sum(&:value) / n] end end end ActiveRecord::Base.establish_connection(adapter: "mysql2", database: "testdb", username: "user", password: "password") columns = %w(value) test_data = TestData.all sma_values = SMA.calculate(test_data, 25).compact SMA.import(columns, sma_values, validate: false)
R
- R 3.5.1
- RMySQL
library(RMySQL) conn <- dbConnect(dbDriver("MySQL"), dbname = "testdb", user = "user", password = "password") dat <- dbReadTable(conn, "test_data") sma <- na.omit(filter(dat$value, rep(1:1, 25))) valuesSQL <- paste( "(NULL, ", sma, ")", sep = "", collapse = "," ) insertSQL <- paste( "INSERT INTO `smas` (`id`, `value`) VALUES", valuesSQL ) dbSendQuery(conn, insertSQL) dbDisconnect(conn)
実行結果
今回試した環境では、Ruby での実装は R の 3.5 倍ほどの時間がかかることがわかった。
> time bundle exec ruby calculate.rb real 0m4.210s user 0m3.175s sys 0m0.129s
> time Rscript --vanilla calculate.R 要求されたパッケージ DBI をロード中です <MySQLResult:-1925505208,0,1> [1] TRUE 警告メッセージ: Closing open result sets real 0m1.273s user 0m0.576s sys 0m0.031s
なお Ruby においては bundle exec ruby -r active_record -r activerecord-import -e ""
だけでも 0.5 秒程度かかるので、実質的な差はより少ないと考えられる。
まとめ
Ruby による実装では R よりも時間がかかったものの、R の 3.5 倍程度の差にとどまるということがわかった。 これは処理対象となるレコードが 10 万件のものなので、レコード数や性能要件によっては Ruby で実装しても問題ないと判断することも多いと思われる。
また Ruby でも ActiveRecord モデルを使用せずレコードのデータを Array の Array でロード・処理することでより高速化はできるかもしれない。
なおここで使用したコードは GitHub に push した (nowlinuxing/activerecord-vs-R) ので興味のある方はどうぞ。