ActiveRecord Relationから特定のカラムの配列を取得したいとき、基本的にはmapではなくpluckを使うべきです。
References
- http://blog.mitchcrowe.com/blog/2012/04/14/10-most-underused-activerecord-relation-methods/
- http://blog.livedoor.jp/sasata299/archives/51847390.html
- http://blog.mitchcrowe.com/blog/2012/04/14/10-most-underused-activerecord-relation-methods/
ただし、pluckはインスタンス化されたオブジェクトに対しても毎回SELECT文を投げてしまうようなので注意です。
pluckを使うべきケース
pluckは必要なカラムのみのSELECTに絞られているため、DB-アプリ間の消費帯域節約やカバリングインデックスが使えた場合などの観点でSQLパフォーマンスが有利です。
また、activerecordオブジェクトを生成せずに値を返すためインスタンス生成オーバーヘッドやメモリ消費の観点でも有利です。
DBから値をとってくる必要があるケースではpluckを使うべきです。
Profile.all.pluck(:id)
# => SELECT "profiles"."id" FROM "profiles"
Profile.all.map(&:id)
# => SELECT "profiles".* FROM "profiles"
Benchmark
n = 2000
Benchmark.bm do |x|
x.report { n.times { Profile.all.pluck(:id) } }
x.report { n.times { Profile.all.map(&:id) } }
end
user | system | total | real | |
---|---|---|---|---|
pluck(:id) | 2.030000 | 2.580000 | 4.610000 | ( 7.576141) |
map(&:id) | 4.420000 | 2.850000 | 7.270000 | ( 10.10201) |
mapを使うべきケース
以下のように既にインスタンス化されたオブジェクトから値をぬきとりたい場合は、pluckだとかなり問題があります。
pluckの場合、毎回SELECT文が発行されてしまうので、ループで実行したりするとN+1問題みたいになります。
(ちなみに、eager_loadやincludeなどをかませてpluckでもSQL発行されない方法を探ってみましたがだめでした。ご存知のかたがいれば教えてください!)
一方mapの場合はメモリ内で計算するだけになりコストはほぼゼロです。
profiles = Profile.all
n = 2000
Benchmark.bm do |x|
x.report { n.times { profiles.pluck(:id) } }
x.report { n.times { profiles.map(&:id) } }
end
user | system | total | real | |
---|---|---|---|---|
pluck(:id) | 2.020000 | 2.400000 | 4.420000 | ( 7.245241) |
map(&:id) | 0.000000 | 0.000000 | 0.000000 | ( 0.007601) |
Environment
※ HostOSはMac OSXで、virtualboxのvagrant NFSモードでCentOS 6.5を起動してます
※ virtualboxだからかGuestOSでのディスクIOがかなり遅いのでたった2000件のテストですごい差が出ています
※ ベンチマークテストでのDBはsqlite3を使ってますが、mysqlでもほぼ同じ結果が出ることを確認しました
pry(main)> Rails.version
=> "4.1.6"
% uname -a
Linux *** 2.6.32-431.el6.x86_64 #1 SMP Fri Nov 22 03:15:09 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
% cat /etc/issue
CentOS release 6.5 (Final)
Kernel \r on an \m