ActiveRecord でランダムなレコードが(1件または複数件)欲しい


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

目次

ランダムに一件取得

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

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

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

シンプルで特定のDBにも依存してないので良さそう。同じ考え方でほかのORMでも通用するはず。

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

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

全件取って、ランダムに一件取り出す、のはダサいしパフォーマンスも悪そう。

Model.all.sample

件数が十分少ないことを保証できるなら簡単だしわかりやすいし、依存性は少ない。要は使い所次第。

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

Stack Overflow
Mongoid random document Lets say I have a Collection of users. Is there a way of using mongoid to find n random users in the collection where it does not return the same user twice? ...

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

が、フルテーブルスキャンでした…

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

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

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

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

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

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

ランダムに複数件取得

今度は、重複しない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) で代用になります。

あわせて読みたい
pluck (ActiveRecord::Calculations) - APIdock

以下、 MongoMapperMongoid の例も。

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 を見直す癖を付けないとダメですね。

Amazon.co.jp
パーフェクト Ruby on Rails 【増補改訂版】
パーフェクト Ruby on Rails 【増補改訂版】
Share it!
目次
閉じる