- 更新日: 2015年3月18日
- Rails
Rails ActiveRecordでランダムにレコードを1件取得する
Rails の ActiveRecord で、MySQL データベースのテーブルから、ランダムにレコードを1件取得するコードを4つの方法で試しました。Rails は便利とはいえ、ActiveRecord が絡む部分でまずい書き方をすると、劇的に動作が遅くなってしまう場合もある。なので、自信がない時は、ちょっと手間がかかりますけどベンチマークを取るようにしてます。
※ 2015/03/18 1つ目のやり方に欠点がありましたので、追記しました。参考にされる場合は、追記部分も読まれるようお願いいたします。
— 環境 —
Rails 4.1
Ruby 2.1
Macbook Air OS X
4種類の書き方を用意
以下4つの方法を書いて試しました。後半で Benchmark 計測。
1つ目
1 |
SomeModel.where( 'id >= ?', rand(SomeModel.first.id..SomeModel.last.id) ).first |
この書き方は、最初に思いついた方法で、おそらく結構速いだろうと見当がついた。以前に、ActiveRecord モデルのインスタンスの前後レコードを取得するコードを書いたことがあり、それに似ていましたので。
Rails ActiveRecordで前後のレコードを取得する | EasyRamble
レコード削除等で id が抜けている可能性があるので、等号による比較は使わないほうが良い。あと、レコード削除等で id が抜けている場合、レコード総数の数値よりレコード最終行の id の数値が大きい可能性があるために、rand の引数には先頭レコード id と最終レコード id の range を渡しています。
単純に SomeModel.count を使う書き方…
1 |
SomeModel.where( 'id >= ?', rand(SomeModel.count) + 1 ).first |
この書き方でも良いけど、id の抜け落ちがあった場合、rand で返される数値に、最後のほうのレコードの id が含まれない。SomeModel.count < SomeModel.last.id になるため。
【追記 2015/03/18】
はてなブックマークでコメントを頂きました。すいません、この1つ目のやり方は欠点がありました。
milk1000cc
1つ目のやり方、id の抜け具合によって出現頻度にばらつきが出るような気が..?
たしかに、id の抜けがあると、特定のレコードの出現頻度が高くなる場合があります。たとえば、レコードの id が “1, 2, 3, 4, 9, 10” となっていたら、5 ~ 8 が抜けていることにより 9 のレコードが出やすくなってしまうはず。なので、このやり方はランダムさの精度をさほど要求されない場合や、id の抜けがない場合にのみ使うほうが良いかと思います。milk1000cc さん、コメントありがとうございました。
それからもう一つ、pluck を使う方法をはてなブックマークでコメント頂きました。
a2ikm
pluck(:id).sampleで得たIDでfindしてた。
pluck を使うやり方もあるのですね。この方法は思いつかなかったので、後半で行ったベンチマークと同条件で計測してみました。
1 2 3 4 5 6 7 8 9 10 |
Benchmark.bm do |b| b.report("pluck:") do 10.times { SomeModel.find( SomeModel.pluck(:id).sample ) } end end user system total real pluck: 17.700000 1.480000 19.180000 ( 20.585349) |
a2ikm さん、コメントありがとうございました。ランダムの正確さが要求される場合は、この pluck か2つ目の offset を使う方法が良いかと思います。
正確なランダムさで、高速に動作する書き方をご存じの方がおられましたら、ぜひご教示ください。
【追記ここまで】
2つ目
1 |
SomeModel.offset( rand(SomeModel.count) ).first |
offset を使う方法は、ぐぐって調べた以下のページで知った。なるほどな〜。
ActiveRecord にてランダムなレコードが欲しい時 | yukku++
3つ目
1 |
SomeModel.all.sample |
これは間違いなく遅い&メモリを圧迫する。この書き方はNGですけど、どのくらい遅いか比較用に用意しました。
4つ目
1 |
SomeModel.order("RAND()").first |
MySQL のネイティブ関数 RAND() を使って、ORDER BY RAND() でクエリーを発行する方法。速いかもしれないけど、MySQL 自体に依存する関数はできたら Rails では使いたくない。一応、後ほど行う比較計測のため。
Rails Quick Tips – Random Records – The Hashrocket Blog
とりあえず pry で実験
どんな SQL が発行されるか確認するために、まずは pry で試す。SQL 発行にかかる時間も出力されるので、ある程度の目安にはなります。試したのは、レコード数が約40万件の MySQL データベースのテーブルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
pry(main)> SomeModel.where( 'id >= ?', rand(SomeModel.first.id..SomeModel.last.id) ).first SomeModel Load (0.7ms) SELECT `some_models`.* FROM `some_models` ORDER BY `some_models`.`id` ASC LIMIT 1 SomeModel Load (0.7ms) SELECT `some_models`.* FROM `some_models` ORDER BY `some_models`.`id` DESC LIMIT 1 SomeModel Load (1.0ms) SELECT `some_models`.* FROM `some_models` WHERE (id >= 130274) ORDER BY `some_models`.`id` ASC LIMIT 1 pry(main)> SomeModel.offset( rand(SomeModel.count) ).first (359.7ms) SELECT COUNT(*) FROM `some_models` SomeModel Load (1049.8ms) SELECT `some_models`.* FROM `some_models` ORDER BY `some_models`.`id` ASC LIMIT 1 OFFSET 404754 pry(main)> SomeModel.all.sample SomeModel Load (2057.1ms) SELECT `some_models`.* FROM `some_models` pry(main)> SomeModel.order("RAND()").first SomeModel Load (3270.1ms) SELECT `some_models`.* FROM `some_models` ORDER BY RAND() LIMIT 1 |
SQL が3回発行されてしまいますけど、やはり一番上の書き方が圧倒的に速いと、この時点で確信しました。SomeModel.first.id, SomeModel.last.id を取得後に使いまわしたりハードコードすれば、SQL のクエリ発行を減らすことはできます。
SomeModel.all.sample は、SQL 実行後のメモリ上の処理でめっちゃ時間かかります。RAND() を使う方法は意外と速くなさそう。
ベンチマークを計測して比較
Ruby の Benchmark モジュールで計測しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Benchmark.bm do |b| b.report("where:") do 10.times { SomeModel.where( 'id >= ?', rand(SomeModel.first.id..SomeModel.last.id) ).first } end b.report("offset:") do 10.times { SomeModel.offset( rand(SomeModel.count) ).first } end b.report("sample:") do 10.times { SomeModel.all.sample } end b.report("order:") do 10.times { SomeModel.order("RAND()").first } end end |
結果。
1 2 3 4 5 6 7 |
user system total real where: 0.020000 0.010000 0.030000 ( 0.032510) offset: 0.020000 0.010000 0.030000 ( 6.496601) sample: 167.200000 9.490000 176.690000 (196.597479) order: 0.010000 0.000000 0.010000 ( 17.528249) |
やはり、一番目の書き方が速い結果に。ということで、
1 |
SomeModel.where( 'id >= ?', rand(SomeModel.first.id..SomeModel.last.id) ).first |
を採用。id が使えない場合は、offset を使う方法ですかね。
- Rails の関連記事
- RailsでMySQLパーティショニングのマイグレーション
- Rails ActiveRecordでdatetime型カラムのGROUP BY集計にタイムゾーンを考慮する
- RailsプラグインGemの作成方法、RSpecテストまで含めたrails pluginの作り方
- RailsでAMPに対応するgemをリリースしました
- Railsでrequest.urlとrequest.original_urlの違い
- Railsでwheneverによるcronバッチ処理
- Google AnalyticsのRails Turbolinks対応
- Railsアプリにソーシャル・シェアボタンを簡単設置
- Rails監視ツール用にErrbitをHerokuで運用
- Facebook APIバージョンのアップグレード手順(Rails OmniAuth)
- 初回公開日: 2015年3月17日
Leave Your Message!