ActiveRecord にてランダムなレコードが欲しい時

ほとんど Rails にどっぷり浸かっているので、データベース操作はもう ActiveRecord に頼りっきりの昨今ですが…。

ランダムに一件取得

全レコードから一件、ランダムに欲しい時。

Model.first(:offset => rand(Model.count))

0からレコード数未満の整数をランダムに作り、それで適当にオフセットする。

シンプルで特定のDBにも依存してないので良さそう。

同じ考え方でほかのORMでも通用するはず。

RDBの関数を呼ぶ方法もあるけど、それだとデータベースに依存しちゃうのでいまいちです。

(SQLITE は RANDOM() 、MySQL は RAND() という関数があるが、関数名が異なる)

全件取って、ランダムに一件取り出す、のはダサい。

Model.all.sample

全件保持するしメモリも喰いそうなので論外。

ちなみに、mongomapper も同様の方法が可能です。mongoid ではできませんでした。

mongoid の場合はなにやら上記リンクでゴリゴリ書いているので、お好きなのを参考にすればいいと思います。

ただしフルテーブルスキャン

このメソッドで生成されるクエリはこんな感じになるわけですが、

SELECT `users`.* FROM `users` LIMIT 1 OFFSET 114354

これを EXPLAIN すると TYPE で ALL (つまりフルスキャン) と出ます。

感覚的には、レコードをみる位置をずらして一件だけ見つける、というような感じなので速そうですが、実はそうではありませんでした。

  • mongoDBとMySQLのlimit offset skip – NullPointer’s Blog
  • offsetの罠:その1:少しだけ対策 – treeのメモ帳

DBチューニングはあんまり得意じゃない(けど身につけなきゃなぁ)ので上記を参考してください。

件数が少ないテーブルならあまり問題にはならないかもしれませんが、パフォーマンスが悪いようであれば他の手段を考慮したほうがいいかもしれません。

(なるべくシンプルであんまりSQLに踏み込まないRAILSっぽい書き方を考え中(募集中…))

ランダムに複数件取得

今度は、重複しない5件を取得したい場合。

Model.find(Model.pluck(:id).shuffle[0..4])

id がシーケンスとは限らないので、一旦全件取り出して、シャッフル、先頭n件を削ってみました。

スコープならこんな感じでしょうか。

scope :random, ->(n){ self.where(id: self.pluck(:id).shuffle[0..n-1]) }

もうちょっと賢くできないのかな…。良い方法があればアドバイスください。

ちなみに pluck メソッドは 3.2.1 以降なので、それよりも低いバージョンでは map(&:id) で代用になります。

MongoMapper と Mongoid の例も。

MongoMapper

Model.all.map {|i| i._id.to_s }.shuffle[0..n-1]

Mongoid

Model.in(_id: Model.all.pluck(:_id).shuffle[0..n-1])

あんまり良い感じに掛けませんでした…。多分もっといいやり方があると思います。

MongoDB はまだあんまり使ったこと無いので…。

全レコードから欲しいレコードを限定せずに一件ないし数件取り出すのは、あまりパフォーマンスがよくありません。

なので高いパフォーマンスが必要ならば、全件からではなく、対象を意図的に絞ったり、なんらかの妥協が必要だと思います。

ActiveRecord は便利ですけど須らく最適な SQL を吐くかどうかは微妙なので、実運用前には大きめのデータを用意してテストして、slow-query を見直す癖を付けないとダメですね。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする