web-dev-qa-db-ja.com

ActiveRecordのランダムレコード

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

ただし、Google検索を介して別の方法に出くわしました(新しいユーザーの制限のため、リンクに関連付けることはできません)。

 Rand_id = Rand(Model.count)
 Rand_record = Model.first(:conditions => ["id >= ?", Rand_id])

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

147
jyunderwood

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

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

offset = Rand(Model.count)

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

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

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

129
Toby Hede

Rails 4および5で、PostgresqlまたはSQLiteを使用し、RANDOM()を使用:

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

おそらく同じことがMySQL with Rand()に対しても機能するでしょう

Model.order('Rand()').first

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

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

180
Mohamad

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

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

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

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

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のオフセットははるかに遅いようです。

EDIT私も試しました

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

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

29
dkam

そんなに難しいことはありません。

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

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

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

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

そして、それによって、どんなユーザーよりも金曜日が好きなランダムなユーザーを選びます。

17
Niels B.

これを処理するRails 3 gemを作成しました。

https://github.com/spilliton/randumb

次のようなことができます:

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

このソリューションを使用することはお勧めしませんが、何らかの理由でreallyデータベースクエリを1つだけ作成しながらレコードをランダムに選択したい場合は、 Ruby配列クラスsampleメソッドを使用できます。これにより、配列からランダムなアイテムを選択できます。

Model.all.sample

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

10
Ryan Atallah

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

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

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

8
Knotty66

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)
6
Thomas Klemm

これらのすべてを読んでも、Rails 5およびMySQL/Maria 5.5を使用して、これらのどれが私の特定の状況で最適に機能するかについて、多くの自信を得ることができませんでした。そこで、65000件のレコードでいくつかの回答をテストし、2つのテイクアウェイを用意しました。

  1. Rand()と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)

この回答は、合成、検証、更新 Mohamedの回答 、および同じことに対するWANGのコメントと受け入れられた回答に関するFlorian Pilzのコメント-それらに投票を送ってください!

5
Sam

Arrayメソッドsampleを使用できます。メソッドsampleは配列からランダムなオブジェクトを返します。使用するには、単純なActiveRecordクエリで実行するだけです。コレクションを返す、例えば:

User.all.sample

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

#<User id: 25, name: "John Doe", email: "[email protected]", created_at: "2018-04-16 19:31:12", updated_at: "2018-04-16 19:31:12">
3
trejo08

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

https://github.com/haopingfan/quick_random_records

このgemを除く他のすべての回答は、大規模なデータベースではパフォーマンスが低下します。

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

enter image description here

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

enter image description here

  1. 受け入れられた回答offsetアプローチコスト245.4ms完全に。

enter image description here

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

enter image description here


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

2
Derek Fan

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

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

Rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: Rand)
2
Yuri Karpovich

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

10列の2,500,000エントリのテーブルを使用すると、勝者の勝者は勝者メソッドのほぼ8倍の速さで勝ちました(オフセット。ローカルサーバーでのみ実行しました。また、これは問題を引き起こす可能性があることに注意する価値があります。これらはそれぞれ一意ではないので、一度に複数の結果を取り出すことができます。

Pluckは25,000,000行のテーブルで100回の実行に勝ちます。編集:実際、この時間にはループのpluckが含まれます。しかしながら;かなりの量の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
Mendoza

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

1
Vadim Eremeev

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

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
Dan Kohn

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
1
mahatmanich

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 module があり、直接返す行数を指定できます。

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
Adam Sheehan

Rails 4.2.8のBenchmarkを使用して、アプリでSamの例を試してみます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
rld

RANDOM()の使用に加えて、これをスコープにスローすることもできます。

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

または、スコープとしてそれを使いたくない場合は、クラスメソッドにスローします。現在、Thing.randomThing.random(n)とともに機能します。

0
Damien Roche

どうするか:

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

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

0
poramo

非常に古い質問ですが、

Rand_record = Model.all.shuffle

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

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

Rand_record = Model.all.shuffle.first
0
Gregdebrick

私はRoRを初めて使いましたが、これは私のために働きました:

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

から来ました:

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

0

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

大きなテーブルの1つのソリューションは、.from("products TABLESAMPLE SYSTEM(0.5)")です。ここで、0.50.5%を意味します。ただし、多くの行を除外するWHERE条件がある場合、この解決策は依然として低速です。 WHERE条件が適用される前にTABLESAMPLE SYSTEM(0.5)すべての行をフェッチするためだと思います。

大きなテーブルの別の解決策は(非常にランダムではありません)です:

products_scope.limit(sample_size).sample(limit)

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

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

0
Linh Dam