ActiveRecordのランダムレコード


151

ActiveRecordを介してテーブルからランダムなレコードを取得する必要があります。私は2006年からのJamis Buckの例に従いました。

ただし、Google検索を介して別の方法に遭遇したこともあります(新しいユーザーの制限により、リンクに属性を付けることはできません)。

 rand_id = rand(Model.count)
 rand_record = Model.first(:conditions => ["id >= ?", rand_id])

ここにいる他の人がどのようにそれをしたのか、または誰かがどの方法がより効率的であるかを知っているのかどうか知りたいです。


2
回答に役立つ2つのポイント。1. IDはどの程度均等に分散されていますか?それらは連続していますか?2.ランダムにする必要がありますか?十分なランダム、または本当のランダム?
マイケル

これらは、activerecordによって自動生成されるシーケンシャルIDであり、それで十分です。
jyunderwood

1
その後、提案されたソリューションは理想に近づきます:)削除された行を少しよく処理するため、COUNT(*)の代わりに「SELECT MAX(id)FROM table_name」を使用します。それ以外の場合は、残りは問題ありません。要するに、「十分に良い」でよければ、実際の分布に近い分布を想定するメソッドが必要になります。それが均一で、あなたが言ったようにさえ、単純なランドはうまく機能します。
マイケル

1
これは、行を削除した場合は機能しません。
Venkat D.

回答:


136

少なくとも2つのクエリなしでこれを行う理想的な方法は見つかりませんでした。

以下は、ランダムに生成された数(現在のレコードカウントまで)をオフセットとして使用します。

offset = rand(Model.count)

# Rails 4
rand_record = Model.offset(offset).first

# Rails 3
rand_record = Model.first(:offset => offset)

正直なところ、私はORDER BY RAND()またはRANDOM()(データベースに応じて)を使用しています。パフォーマンスの問題がなければ、パフォーマンスの問題ではありません。


2
コードModel.find(:offset => offset).firstはエラーをスローします。Model.first(:offset => offset)パフォーマンスが良くなると思います。
Harish Shetty

1
ええ、私はRails 3を使用していて、バージョン間のクエリ形式について混乱し続けています。
Toby Hede

7
大規模なデータセットでは、実際にインデックススキャン(またはクラスター化インデックスがInnoDBのように使用されている場合はテーブルスキャン)が必要なため、オフセットの使用は非常に遅いことに注意してください。つまり、O(N)操作ですが、「WHERE id> =#{rand_id} ORDER BY id ASC LIMIT 1」はO(log N)であり、はるかに高速です。
ケン、

15
オフセットアプローチでは、ランダムに検出された単一のデータポイントしか生成されないことに注意してください(最初のデータポイントは、すべてIDでソートされています)。ランダムに選択された複数のレコードが必要な場合は、このアプローチを複数回使用するか、データベースによって提供されるランダムな順序方法を使用する必要がありThing.order("RANDOM()").limit(100)ます。(RANDOM()PostgreSQLとRAND()MySQLにあることに注意してください...望むほど移植性がありません。)
Florian Pilz

3
Rails 4では動作しません。を使用してくださいModel.offset(offset).first
mahemoff 2014

206

Rails 6

Jasonがコメントで述べたように、Rails 6では、属性以外の引数は許可されていません。値はArel.sql()ステートメントでラップする必要があります。

Model.order(Arel.sql('RANDOM()')).first

Rails 5、4

レール4及び5用いて、PostgresqlのまたはSQLiteのを、使用しましたRANDOM()

Model.order('RANDOM()').first

以下のために働くだろうおそらく同じMySQLのRAND()

Model.order('RAND()').first

これ受け入れられた回答のアプローチより約2.5倍高速です。

警告:これは、数百万のレコードを持つ大規模なデータセットの場合は遅いため、limit句を追加する必要がある場合があります。


4
「Random()」はsqliteでも機能するため、まだsqliteで開発していて、本番環境でpostgresを実行している私たちにとって、ソリューションは両方の環境で機能します。
wuliwong 2014年

5
受け入れられた回答に対するこのベンチマークを作成しました。Postgresql 9.4では、この回答のアプローチは約2倍高速です。
panmari 2015年


これが最速のソリューションです
セルジオBelevskij

1
「非属性引数はRails 6.0では許可されません。このメソッドは、リクエストパラメータやモデル属性などのユーザー指定の値で呼び出すことはできません。既知の安全な値は、Arel.sql()でラップすることで渡すことができます。」
トレントンタイラー

73

レコードが削除されると、サンプルコードは不正確に動作し始めます(IDが小さいアイテムを不当に優先します)

データベース内でランダムな方法を使用する方がよいでしょう。これらは使用しているDBによって異なりますが、:order => "RAND()"はmysqlで機能し、:order => "RANDOM()"はpostgresで機能します

Model.first(:order => "RANDOM()") # postgres example

7
MySQLのORDER BY RAND()は、データが増加するにつれて、恐ろしいランタイムになります。数千行から始めても(時間要件に応じて)維持できません。
マイケル、

マイケルは素晴らしい点を持ち出します(他のDBについても同様です)。一般に、大きなテーブルからランダムな行を選択することは、動的アクションで実行したいことではありません。キャッシングはあなたの友達です。あなたが達成しようとしていることを再考することも悪い考えではないかもしれません。
semanticart

1
約100万行のテーブルでmysqlのRAND()を順序付けることは、slooooooooooooooooooooowです。
サブイメージ2011

24
もう機能しません。Model.order("RANDOM()").first代わりに使用してください。
phil pirozhkov 2013

低速でデータベース固有。ActiveRecordはデータベース間でシームレスに機能するため、このメソッドは使用しないでください。
Dex

29

+5.10万レコードの製品テーブルでのMySQL 5.1.49、Ruby 1.9.2p180でのこれら2つのメソッドのベンチマーク:

def random1
  rand_id = rand(Product.count)
  rand_record = Product.first(:conditions => [ "id >= ?", rand_id])
end

def random2
  if (c = Product.count) != 0
    Product.find(:first, :offset =>rand(c))
  end
end

n = 10
Benchmark.bm(7) do |x|
  x.report("next id:") { n.times {|i| random1 } }
  x.report("offset:")  { n.times {|i| random2 } }
end


             user     system      total        real
next id:  0.040000   0.000000   0.040000 (  0.225149)
offset :  0.020000   0.000000   0.020000 ( 35.234383)

MySQLのオフセットははるかに遅いようです。

編集 私も試しました

Product.first(:order => "RAND()")

しかし、私はそれを約60秒後に殺さなければなりませんでした。MySQLは「ディスク上のtmpテーブルにコピーしています」。それはうまくいきません。


1
より多くのテストを探している人のために、実際のランダムなアプローチにかかる時間:Thing.order("RANDOM()").first250kエントリのテーブルを試しました-クエリは0.5秒未満で終了しました。(PostgreSQL 9.0、REE 1.8.7、2 x 2.66 GHzコア)1回限りの「クリーンアップ」を実行しているため、これは十分高速です。
フロリアンピルツ

6
Rubyのrandメソッドは、指定された数値より1つ少ない値を返すため、必要な場合rand_id = rand(Product.count) + 1や、最後のレコードを取得できない場合があります。
リッチー

4
random1テーブルの行を削除した場合、メモは機能しません。(カウントは最大ID未満であり、高いIDの行を選択することはできません)。
Nicholas

使用することrandom2によって改善することができる#orderインデックス付きのカラムを使用して。
Carson Reinke、2014

18

それほど難しいことではありません。

ids = Model.pluck(:id)
random_model = Model.find(ids.sample)

pluckテーブル内のすべてのIDの配列を返します。sample配列のメソッドは、配列からランダムなIDを返します。

これは、選択の確率が等しく、行が削除されたテーブルをサポートすることで、うまく機能するはずです。制約と組み合わせることもできます。

User.where(favorite_day: "Friday").pluck(:id)

そして、それによって、任意のユーザーだけでなく、金曜日が好きなランダムなユーザーを選びます。


8
これはクリーンで、小さなテーブルや1回限りの使用で機能しますが、スケーリングされないことに注意してください。3Mテーブルでは、MariaDBでIDを取得するのに約15秒かかります。
mahemoff 2014年

2
それは良い点です。同じ品質を維持しながら、より高速な代替ソリューションを見つけましたか?
Niels B. 14年

受け入れられているオフセットソリューションは同じ品質を維持していませんか?
mahemoff 14年

いいえ、これは条件をサポートしておらず、レコードが削除されたテーブルを選択する確率は同じではありません。
Niels B.

1
考えてみてください。カウントとオフセットの両方を選択するときに制約を適用すると、この手法が機能するはずです。数えるだけのことを想像していた。
Niels B.

15

このソリューションを使用することはお勧めしませんが、何らかの理由で実際にデータベースクエリを1つだけ実行しながらランダムにレコードを選択する場合はsampleRuby Arrayクラスのメソッドを使用して、ランダムなアイテムを選択できます。配列から。

Model.all.sample

この方法はデータベースクエリのみModel.offset(rand(Model.count)).firstを必要としますが、2つのデータベースクエリを必要とするような方法よりもかなり低速ですが、後者の方がより好ましいです。


99
こんなことしないで。ずっと。
Zabba 2012年

5
データベースに10万行ある場合、これらすべてをメモリにロードする必要があります。
Venkat D.

3
もちろん、実稼働のリアルタイムコードには推奨されませんが、このソリューションは気に入っています。偽の値でデータベースにシードするなどの特別な状況で使用することは非常に明確です。
fguillen

13
お願い-決して言わないでください。これは、テーブルが小さい場合の開発時のデバッグに最適なソリューションです。(そして、サンプルを取得している場合、デバッグはおそらくユースケースです)。
mahemoff 2013

私は種まきに使用していて、私には良いです。さらに、Model.all.sample(n)も機能します:)
Arnaldo Ignacio GasparVéjar13年

13

これを処理するために、レール3の宝石を作成しました。

https://github.com/spilliton/randumb

それはあなたがこのようなことをすることを可能にします:

Model.where(:column => "value").random(10)

7
このgemのドキュメントでは、「randumbは単にクエリに追加ORDER BY RANDOM()(またはRAND()mysqlの場合)を追加すると説明しています。–したがって、@ semanticartの回答に対するコメントで言及されているパフォーマンスの低下に関するコメントは、このgemを使用するときにも適用されます。しかし、少なくともDBからは独立しています。
ニコラス

8

私はこれをコンソールから頻繁に使用しています。初期化子-Rails 4の例でActiveRecordを拡張します。

class ActiveRecord::Base
  def self.random
    self.limit(1).offset(rand(self.count)).first
  end
end

その後、呼び出しFoo.randomてランダムなレコードを取り戻すことができます。


1
必要limit(1)ですか?ActiveRecord#firstそれを行うのに十分スマートでなければなりません。
tokland

6

Postgresの1つのクエリ:

User.order('RANDOM()').limit(3).to_sql # Postgres example
=> "SELECT "users".* FROM "users" ORDER BY RANDOM() LIMIT 3"

オフセットを使用して、2つのクエリ:

offset = rand(User.count) # returns an integer between 0 and (User.count - 1)
Model.offset(offset).limit(1)

1
-1は不要で、randはnum-1までカウントされます
anemaria20

ありがとう、変更されました:+1:
Thomas Klemm 2016年

5

これらすべてを読んでも、Rails 5とMySQL / Maria 5.5を使用する私の特定の状況で、これらのどれが最適に機能するかについては、大きな自信が持てませんでした。だから私は65000レコードまでの回答のいくつかをテストし、2つのポイントを持っています:

  1. RAND()とa limitは明らかに勝者です。
  2. pluck+ は使用しないでくださいsample
def random1
  Model.find(rand((Model.last.id + 1)))
end

def random2
  Model.order("RAND()").limit(1)
end

def random3
  Model.pluck(:id).sample
end

n = 100
Benchmark.bm(7) do |x|
  x.report("find:")    { n.times {|i| random1 } }
  x.report("order:")   { n.times {|i| random2 } }
  x.report("pluck:")   { n.times {|i| random3 } }
end

              user     system      total        real
find:     0.090000   0.000000   0.090000 (  0.127585)
order:    0.000000   0.000000   0.000000 (  0.002095)
pluck:    6.150000   0.000000   6.150000 (  8.292074)

この回答は、モハメドの回答と、ナミワンの同じコメントに対するコメント、およびフローリアンピルツの承認された回答に対するコメントを合成、検証、および更新します。投票を送信してください。


3

あなたは使用することができるArray方法をsample方法は、sampleそれあなただけのシンプルでexecをする必要が使用するためには、配列からランダムにオブジェクトを返しますActiveRecordたとえば、コレクションを返すクエリを:

User.all.sample

このようなものを返します:

#<User id: 25, name: "John Doe", email: "admin@example.info", created_at: "2018-04-16 19:31:12", updated_at: "2018-04-16 19:31:12">

ARを使用しているときに配列メソッドを使用することはお勧めしません。この方法では、order('rand()').limit(1)「同じ」ジョブ(約10Kレコード)を実行するのに約8倍の時間がかかります。
セバスチャンパルマ

3

ランダムレコードには、この宝石を強くお勧めします。これは、大量のデータ行を持つテーブル用に特別に設計されています。

https://github.com/haopingfan/quick_random_records

他のすべての回答は、このgemを除いて、大規模なデータベースではうまく機能しません。

  1. quick_random_recordsは4.6ms全体のコストのみです。

ここに画像の説明を入力してください

  1. User.order('RAND()').limit(10)コスト733.0ms

ここに画像の説明を入力してください

  1. 受け入れられた回答offsetアプローチは245.4ms完全に費用がかかります。

ここに画像の説明を入力してください

  1. User.all.sample(10)アプローチコスト573.4ms

ここに画像の説明を入力してください


注:テーブルには120,000ユーザーしかありません。レコード数が多いほど、パフォーマンスの差は大きくなります。


2

指定されたスコープ内でランダムな結果を選択する必要がある場合:

scope :male_names, -> { where(sex: 'm') }
number_of_results = 10

rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)

1

リストからランダムにアイテムを選ぶRubyメソッドはsampleです。sampleActiveRecordの効率を作成するために、以前の回答に基づいて、私は以下を使用しました:

module ActiveRecord
  class Base
    def self.sample
      offset(rand(size)).first
    end
  end
end

私はこれを入れてからこれlib/ext/sample.rbを次の場所にロードしますconfig/initializers/monkey_patches.rb

Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }

これは、モデルのサイズがすでにキャッシュされている場合は1つのクエリになり、それ以外の場合は2つになります。


1

Rails 4.2とOracle

Oracleの場合、次のようにモデルにスコープを設定できます。

scope :random_order, -> {order('DBMS_RANDOM.RANDOM')}

または

scope :random_order, -> {order('DBMS_RANDOM.VALUE')}

そして、サンプルでは次のように呼び出します:

Model.random_order.take(10)

または

Model.random_order.limit(5)

もちろん、次のようなスコープなしで注文することもできます。

Model.all.order('DBMS_RANDOM.RANDOM') # or DBMS_RANDOM.VALUE respectively

これはpostgres with order('random()'とMySQL order('rand()')でも可能です。これは間違いなく最良の答えです。
jrochkind

1

MySQLデータベースの場合:Model.order( "RAND()")。first


これはmysqlでは機能しません。少なくとも、このDBが動作すると想定しているDBエンジンをクラウド化する必要があります
Arnold Roa

申し訳ありませんが、タイプミスがありました。今修正されました。mysqlで動作するはずです(のみ)
Vadim Eremeev

1

PostgreSQL 9.5以降を使用している場合は、 TABLESAMPLE、ランダムなレコードを選択するのにます。

2つのデフォルトのサンプリング方法(SYSTEMおよびBERNOULLI)では、返す行数をテーブル内の合計行数のパーセンテージとして指定する必要があります。

-- Fetch 10% of the rows in the customers table.
SELECT * FROM customers TABLESAMPLE BERNOULLI(10);

これには、適切なパーセンテージを選択するために、テーブル内のレコードの量を知る必要がありますが、すぐに見つけるのは簡単ではありません。幸いなことに、直接返す行数を指定できるtsm_system_rowsモジュールがあります

CREATE EXTENSION tsm_system_rows;

-- Fetch a single row from the customers table.
SELECT * FROM customers TABLESAMPLE SYSTEM_ROWS(1);

これをActiveRecord内で使用するには、最初に移行内で拡張機能を有効にします。

class EnableTsmSystemRowsExtension < ActiveRecord::Migration[5.0]
  def change
    enable_extension "tsm_system_rows"
  end
end

次にfrom、クエリの句を変更します。

customer = Customer.from("customers TABLESAMPLE SYSTEM_ROWS(1)").first

SYSTEM_ROWSサンプリング方法が完全にランダムになるのか、それともランダムなページから最初の行を返すだけなのかはわかりません。

この情報のほとんどは、Gulcin Yildirimが作成した2ndQuadrantブログの投稿から取得されました。


1

非常に多くの回答を見た後、私はそれらすべてをPostgreSQL(9.6.3)データベースでベンチマークすることにしました。より小さな100,000テーブルを使用し、Model.order( "RANDOM()")。firstを取り除きました。

2,500,000エントリ、10列のテーブルを使用した場合、手に負えた勝者はpluckメソッドがrunner up(offset。メソッドは、私が最終的に使用するものです。これは、問題の原因となる可能性があることにも注意する必要があります。一度に複数の結果を取得するのは、それらの1つ1つが一意であり、ランダム性が低いためです。

Pluckは、25,000,000行のテーブルで100回実行された勝ちです。編集:実際には、この時間にループ内のPluckが含まれています。これを実行すると、IDの単純な反復とほぼ同じくらい高速に実行されます。しかしながら; かなりの量のRAMを消費します。

RandomModel                 user     system      total        real
Model.find_by(id: i)       0.050000   0.010000   0.060000 (  0.059878)
Model.offset(rand(offset)) 0.030000   0.000000   0.030000 ( 55.282410)
Model.find(ids.sample)     6.450000   0.050000   6.500000 (  7.902458)

これは、ランダムを除外するために100,000行のテーブルで2000回実行されているデータです。

RandomModel       user     system      total        real
find_by:iterate  0.010000   0.000000   0.010000 (  0.006973)
offset           0.000000   0.000000   0.000000 (  0.132614)
"RANDOM()"       0.000000   0.000000   0.000000 ( 24.645371)
pluck            0.110000   0.020000   0.130000 (  0.175932)

1

非常に古い質問ですが、

rand_record = Model.all.shuffle

ランダムな順序で並べ替えて、レコードの配列を取得しました。宝石やスクリプトは必要ありません。

1つのレコードが必要な場合:

rand_record = Model.all.shuffle.first

1
これはすべてのレコードをメモリにロードするため、最適なオプションではありません。また、shuffle.first==.sample
Andrew Rozhenko

0

私はRoRを初めて使用しましたが、これでうまくいきました。

 def random
    @cards = Card.all.sort_by { rand }
 end

それはから来ました:

Rubyで配列をランダムにソート(スクランブル)する方法は?


4
それの悪い点は、データベースからすべてのカードをロードすることです。データベース内で行う方が効率的です。
アントンクズミン2013年

で配列をシャッフルすることもできますarray.shuffle。とにかく、注意してください。Card.allすべてのカードレコードがメモリに読み込まれるので、話しているオブジェクトが多いほど非効率になります。
Thomas Klemm 2013

0

どうするか:

rand_record = Model.find(Model.pluck(:id).sample)

私にとっては非常に明確です


0

ベンチマークのレール4.2.8を使用して、私のアプリでサムの例を試してみました(ランダムに1..Category.countを設定しました。ランダムが0になるとエラーが発生するためです(ActiveRecord :: RecordNotFound:見つかりませんでした'id' = 0))のカテゴリで、地雷は次のとおりでした。

 def random1
2.4.1 :071?>   Category.find(rand(1..Category.count))
2.4.1 :072?>   end
 => :random1
2.4.1 :073 > def random2
2.4.1 :074?>    Category.offset(rand(1..Category.count))
2.4.1 :075?>   end
 => :random2
2.4.1 :076 > def random3
2.4.1 :077?>   Category.offset(rand(1..Category.count)).limit(rand(1..3))
2.4.1 :078?>   end
 => :random3
2.4.1 :079 > def random4
2.4.1 :080?>    Category.pluck(rand(1..Category.count))
2.4.1 :081?>
2.4.1 :082 >     end
 => :random4
2.4.1 :083 > n = 100
 => 100
2.4.1 :084 > Benchmark.bm(7) do |x|
2.4.1 :085 >     x.report("find") { n.times {|i| random1 } }
2.4.1 :086?>   x.report("offset") { n.times {|i| random2 } }
2.4.1 :087?>   x.report("offset_limit") { n.times {|i| random3 } }
2.4.1 :088?>   x.report("pluck") { n.times {|i| random4 } }
2.4.1 :089?>   end

                  user      system      total     real
find            0.070000   0.010000   0.080000 (0.118553)
offset          0.040000   0.010000   0.050000 (0.059276)
offset_limit    0.050000   0.000000   0.050000 (0.060849)
pluck           0.070000   0.020000   0.090000 (0.099065)

0

.order('RANDOM()').limit(limit)きちんと見えlimitますが、1であってもすべての行をフェッチしてソートする必要があるため、大きなテーブルでは遅くなります(内部的にはデータベース内にあり、Rails内にはありません)。MySQLについてはわかりませんが、これはPostgresで発生します。ここここでより多くの説明。

大きなテーブルの解決策の1つは.from("products TABLESAMPLE SYSTEM(0.5)")、の0.5意味0.5%です。ただし、WHERE多くの行をフィルターで除外する条件がある場合、この解決策はまだ遅いことがわかります。条件が適用されるTABLESAMPLE SYSTEM(0.5)前にすべての行をフェッチするためだと思いWHEREます。

大きなテーブル(ランダムではない)の別の解決策は次のとおりです。

products_scope.limit(sample_size).sample(limit)

where sample_size100(ただし、大きすぎない場合は遅くなり、大量のメモリを消費します)、およびにlimitすることができます1。これは高速ですが実際にはランダムではありませんが、sample_sizeレコード内でのみランダムであることに注意してください。

PS:DBキャッシュのおかげで、2回目に実行される一部のDBクエリは1回目に実行するよりも大幅に高速になる可能性があるため、上記の回答のベンチマーク結果は(少なくともPostgresでは)信頼できません。そして残念ながら、これらのベンチマークを信頼できるものにするためにPostgresのキャッシュを無効にする簡単な方法はありません。


0

を使用RANDOM()するだけでなく、これをスコープにスローすることもできます。

class Thing
  scope :random, -> (limit = 1) {
    order('RANDOM()').
    limit(limit)
  }
end

または、スコープとしてそれを気に入らない場合は、クラスメソッドにスローするだけです。今、Thing.random一緒に動作しますThing.random(n)


0

「ランダム」の意味と実際にやりたいことに応じて、 take十分かもしれません。

ランダムの「意味」とは、次のことを意味します。

  • 私が気にしない要素を私に与えるという意味ですか?それで十分です
  • ここで、「実験を繰り返すことでセットから異なる要素が得られる可能性がかなり高い要素を与えてください」という意味の場合は、他の回答で述べられている方法のいずれかで「運」を強制します。

例では、テストするために、サンプルデータがランダムにとにかく作成されている可能性があり、そうtake十二分である、とさえ、正直に言うとfirst

https://guides.rubyonrails.org/active_record_querying.html#take

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.