今更だけどrailsのmapとpluckを比較してみた
あけましておめでとうございます。
キーボードと戯れていたら2020年を迎えることができました。mendです。
就職してサーバーサイドエンジニアとして働いていて、現在のお仕事ではRailsを使用しております。
学生時代はRailsでの開発経験がなく、かなり浅い知識で仕事に取り組む中でmodel
の特定カラムの情報を取得する部分のレビューに
??? 「map
は重いからやめたほうがいいよ」
??? 「ここ(map
)、pluck
に変えたほうがいいですよ」
とのご指摘をもらうことがよくありました。
帰省した実家で退屈をしているのもあって今回の記事では普段癖で書いてしまうmap
よりも、本当にpluck
のほうが軽いのかを検証してみたいと思います。
検証環境
デフォルト設定です
- Ruby on Rails: 5.2.3
- データベース: SQLite
忙しい人のためにまずは結論
処理時間
件数 | 100 | 1000 | 10000 | 100000 |
---|---|---|---|---|
map |
0.002795s | 0.019125s | 0.125839s | 1.529347s |
pluck |
0.000799s | 0.001634s | 0.015934s | 0.164019s |
結果:map
が重い
すごく当たり前ですが、やはりレコードが多くなるにつれて処理にかかる時間が長くなりました。
rakeタスクを作成する
いちいちコンソール叩いて検証するのも面倒なのでタスクを作成して検証しました。
$bundle exec rails generate task method_performance_test
このコマンドでlib/tasks/method_perforance_test.rake
が作成されるので、こちらにタスクとして行う処理を書いていきます。
railsでモデルを扱うタスクの場合、:environment
が必要みたいなので書き足しておきました。
task method_performance_test: :environment do |_task|
# ここに処理を書いていく
end
検証用モデルの作成
map
とpluck
の検証に使用するモデルは結果的にカラムの情報が取得できればいいので特に何も気にせず作成しました。
とりあえず今回の記事では文字列を格納するname
のカラムを持つUser
を作成しました。
$ bundle exec rails g model User name:string
これでモデルとマイグレーションファイルが作成されるので
$ bundle exec rails db:migrate
マイグレーションを忘れずに行いましょう。個人的にはridgepole
のほうが好きです。
rakeタスクの実装
ユーザー全件の削除
今回は検証として行うので、きちんとレコードを空にしてから処理を行うようにします。
全部消すのは簡単なのでササッと処理を書いちゃいます。
# ユーザー全件を削除する
User.all.map { |user| user.destroy! }
うわこっわ....
task内で環境変数を呼び出す
レコード数の件数ごとにパフォーマンスの測定を行いたいので、コマンド実行時に環境変数を参照できるようにしたいと思います。
$ bundle exec rake method_performance_test SIZE=10
このように最後に付け足すことで、タスク内で環境変数を呼び出せるようにします
puts "#{ENV['SIZE'].to_i}" # 10
検証用のレコードを作成する
先程の環境変数から受け取った値の件数だけレコードを作成できるようにします
num = ENV['SIZE'].to_i
num.times do |n|
User.create(name: "test#{num.to_s.rjust(7, "0")}")
end
map
とpluck
の処理時間を計測する
実行時間の計測自体は結構シンプルです。
現在時刻から計測開始時刻を引くだけです。
# mapの処理時間を計測する
start_time = Time.current
User.all.map(&:id)
map_time = Time.current - start_time
# pluckの処理時間を計測する
start_time = Time.current
User.all.pluck(:id)
pluck_time = Time.current - start_time
作成したタスク
最終的にこんな感じになりました
task method_performance_test: :environment do |_task|
puts "user_size:#{ENV['SIZE'].to_i}"
num = ENV['SIZE'].to_i
puts "=====initializing====="
User.all.map { |user| user.destroy! }
puts "=====create users: size = #{num}====="
insert_time = Time.current
num.times do |n|
User.create!(name: "test#{num.to_s.rjust(7, "0")}")
end
puts "=====create users: done====="
puts " "
puts "INSERT time: #{Time.current - insert_time}s"
puts " "
puts "=====user.all.map(&:id): start====="
puts "=====class:#{User.all.map(&:id).class}====="
start_time = Time.current
User.all.map(&:id)
map_time = Time.current - start_time
puts "result map: #{map_time}s"
puts "=====user.all.map(&:id): end====="
puts "=====user.all.pluck(:id): start====="
puts "=====class:#{User.all.pluck(:id).class}====="
start_time = Time.current
User.all.pluck(:id)
pluck_time = Time.current - start_time
puts "result pluck: #{pluck_time}s"
puts "=====user.all.pluck(:id): end====="
puts "=====clean up test data====="
delete_time = Time.current
User.all.map { |user| user.destroy! }
puts " "
puts "DELETE time: #{Time.current - delete_time}s"
puts " "
puts "=====result====="
puts "map :#{map_time}s"
puts "pluck :#{pluck_time}s"
end