Rails 3:ランダムなレコードを取得する


132

したがって、Rails 2でランダムなレコードを見つける例をいくつか見つけました。推奨される方法は次のようです。

Thing.find :first, :offset => rand(Thing.count)

初心者なので、Rails 3の新しい検索構文を使用してこれをどのように構築できるかわかりません。

では、ランダムなレコードを見つけるための「Rails 3 Way」とは何でしょうか。



9
^^ただし、Rails 3の最適な方法を具体的に探していますが、これが質問の目的です。
Andrew

rails 3固有はクエリチェーンのみです:)
fl00r

回答:


216
Thing.first(:order => "RANDOM()") # For MySQL :order => "RAND()", - thanx, @DanSingerman
# Rails 3
Thing.order("RANDOM()").first

または

Thing.first(:offset => rand(Thing.count))
# Rails 3
Thing.offset(rand(Thing.count)).first

実際、Rails 3ではすべての例が機能します。しかしRANDOM、大きなテーブルでは順序の使用はかなり遅くなりますが、よりSQLスタイル

UPD。インデックス付きの列で次のトリックを使用できます(PostgreSQL構文)。

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;

11
ただし、最初の例はMySQLでは機能しません-MySQLの構文はThing.first(:order => "RAND()")です(ActiveRecord抽象化を使用するのではなくSQLを作成する危険)
DanSingerman

@ DanSingerman、はい、それはDB固有RAND()またはRANDOM()です。ありがとう
fl00r 2011年

そして、インデックスから欠落しているアイテムがある場合、これは問題を作成しませんか?(スタックの途中で何かが削除された場合、それが要求される可能性はありますか?
ビクターS

@VictorS、いいえ、#offsetは次の利用可能なレコードに移動しません。Ruby 1.9.2とRails 3.1でテストしました
SooDesuNe

1
@JohnMerlino、はい0はIDではなくオフセットです。Offet 0は、注文に応じて最初のアイテムを意味します。
fl00r 2014

29

私はプロジェクトに取り組んでいます(Rails 3.0.15、ruby 1.9.3-p125-perf)。ここで、dbはlocalhostにあり、usersテーブルには100Kを少し超えるレコードがあります。

使用する

RAND()による順序

かなり遅い

User.order( "RAND(id)")。first

なる

SELECT users。* FROM usersORDER BY RAND(id)LIMIT 1

以下とから取り812秒の応答に!!

Railsログ:

ユーザーロード(11030.8ms)SELECT users。* FROM usersORDER BY RAND()LIMIT 1

mysqlの説明から

+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra                           |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 110165 | Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

インデックスが使用されておらず(possible_keys = NULL)、一時テーブルが作成されており、目的の値をフェッチするために追加のパスが必要であることがわかります(extra =一時的な使用; filesortの使用)。

一方、クエリを2つの部分に分割し、Rubyを使用することで、応答時間を合理的に改善できます。

users = User.scoped.select(:id);nil
User.find( users.first( Random.rand( users.length )).last )

(;コンソール使用の場合はnil)

Railsログ:

ユーザーロード(25.2ms)SELECT id FROM usersユーザーロード(0.2ms)SELECT users。* FROM usersWHERE usersid= 106854制限1

そしてmysqlの説明は理由を証明します:

+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| id | select_type | table | type  | possible_keys | key                      | key_len | ref  | rows   | Extra       |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | index | NULL          | index_users_on_user_type | 2       | NULL | 110165 | Using index |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+

+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

これで、インデックスと主キーのみを使用して、約500倍速く処理できるようになりました。

更新:

コメントでicantbecoolによって指摘されているように、テーブルに削除されたレコードがある場合、上記のソリューションには欠陥があります。

その回避策は

users_count = User.count
User.scoped.limit(1).offset(rand(users_count)).first

2つのクエリに変換されます

SELECT COUNT(*) FROM `users`
SELECT `users`.* FROM `users` LIMIT 1 OFFSET 148794

約500msで実行されます。


2番目の例の「最後」の後に「.id」を追加すると、「IDのないモデルが見つかりませんでした」というエラーが回避されます。例:User.find(users.first(Random.rand(users.length))。last.id)
turing_machine

警告!MySQLでは、クエリごとに異なるランダムな順序を与えることRAND(id)はありませんRAND()クエリごとに異なる順序が必要な場合に使用します。
Justin Tanner

User.find(users.first(Random.rand(users.length))。last.id)は、レコードが削除されていると機能しません。[1,2,4,5、]で、IDとして3を選択する可能性がありますが、アクティブなレコードの関係はありません。
icantbecool 2015年

また、users = User.scoped.select(:id); nilは非推奨ではありません。代わりにこれを使用してください:users = User.where(nil).select(:id)
icantbecool

最初のパラメータとしてRandom.rand(users.length)を使用するのはバグだと思います。Random.randは0を返すことができます。最初に0をパラメーターとして使用すると、制限はゼロに設定され、これはレコードを返しません。何1の代わりに使用すべきことである1 +ランダム(users.length)と仮定しusers.length> 0
SWoo

12

Postgresを使用している場合

User.limit(5).order("RANDOM()")

MySQLを使用している場合

User.limit(5).order("RAND()")

どちらの場合も、ユーザーテーブルからランダムに5つのレコードを選択しています。これは、コンソールに表示される実際のSQLクエリです。

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

11

これを行うために、大きなテーブルでより優れたパフォーマンスを発揮し、関係とスコープをチェーンできるようにするために、rails 3 gemを作成しました。

https://github.com/spilliton/randumb

(編集):私のgemのデフォルトの動作は基本的に上記と同じアプローチを使用していますが、必要に応じて古い方法を使用することもできます:)


6

投稿された回答の多くは、実際にはかなり大きなテーブル(100万行以上)ではうまく機能しません。ランダムな順序付けはすぐに数秒で完了し、テーブルでカウントを行うのにも非常に時間がかかります。

この状況で私にとってうまくいく解決策はRANDOM()、where条件で使用することです。

Thing.where('RANDOM() >= 0.9').take

100万行を超えるテーブルでは、このクエリの所要時間は通常2ミリ秒未満です。


ソリューションの別の利点は、クエリtakeを提供LIMIT(1)するが配列ではなく単一の要素を返す関数を使用することです。したがって、呼び出す必要はありませんfirst
Piotr Galas 2018

テーブルの最初のレコードでは、この方法で選択された確率が高くなっているようです。
18

5

さあ行こう

レールウェイ

#in your initializer
module ActiveRecord
  class Base
    def self.random
      if (c = count) != 0
        find(:first, :offset =>rand(c))
      end
    end
  end
end

使用法

Model.random #returns single random object

または第二の考えは

module ActiveRecord
  class Base
    def self.random
      order("RAND()")
    end
  end
end

使用法:

Model.random #returns shuffled collection

Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno

ユーザーがいないために2を取得したい場合は、エラーが発生します。理にかなっています。
Tim Kretschmer

1
2番目のアプローチはpostgresでは機能しませんが、"RANDOM()"代わりに使用できます...
Daniel Richter

4

これは私にとって非常に役に立ちましたが、もう少し柔軟性が必要だったので、これは私がやったことです:

ケース1:ランダムなレコードソースを1つ見つける:Trevor turkサイト
これをThing.rbモデルに追加する

def self.random
    ids = connection.select_all("SELECT id FROM things")
    find(ids[rand(ids.length)]["id"].to_i) unless ids.blank?
end

それからあなたのコントローラーであなたはこのようなものを呼び出すことができます

@thing = Thing.random

ケース2:複数のランダムレコード を検索する(繰り返しなし)ソース:繰り返しのない
10個のランダムレコードを見つける必要があることを思い出せないので、これが機能していることがわかりました

thing_ids = Thing.find( :all, :select => 'id' ).map( &:id )
@things = Thing.find( (1..10).map { thing_ids.delete_at( thing_ids.size * rand ) } )

これにより10個のランダムなレコードが見つかりますが、データベースが特に大きい場合(数百万のレコード)、これは理想的ではなく、パフォーマンスが低下することを言及する価値があります。私にとっては十分な数千レコードまで十分に機能します。


4

リストからランダムにアイテムを選択する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 }

実際に#countは、のDBを呼び出しますCOUNT。レコードが既にロードされている場合、これは悪い考えかもしれません。使用#sizeするかどうか#count、またはレコードがすでにロードされている場合はを使用するかどうかを決定するため、代わりにリファクタリングを使用します#length
BenMorganIO 2015

フィードバックcountsize基づいてからに切り替えました。詳細情報:dev.mensfeld.pl/2014/09/...
ダンコーン

3

Rails 5で動作し、DBに依存しません。

これはあなたのコントローラーで:

@quotes = Quote.offset(rand(Quote.count - 3)).limit(3)

もちろん、ここに示すように、これを考慮に入れることができます

app / models / concerns / randomable.rb

module Randomable
  extend ActiveSupport::Concern

  class_methods do
    def random(the_count = 1)
      records = offset(rand(count - the_count)).limit(the_count)
      the_count == 1 ? records.first : records
    end
  end
end

その後...

app / models / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

その後、次のようにするだけで使用できます。

Books.random

または

Books.random(3)

これは常に、少なくとも文書化する必要がある後続のレコードを取得します(ユーザーが望んでいない場合があるため)。
18

2

ActiveRecordでsample()を使用できます

例えば

def get_random_things_for_home_page
  find(:all).sample(5)
end

出典:http : //thinkingeek.com/2011/07/04/easily-select-random-records-rails/


33
これは、大量のレコードがある場合に使用する非常に悪いクエリです。DBはすべてのレコードを選択し、Railsはそこから5つのレコードを選択します-非常に無駄です。
DaveStephens 2013年

5
sampleActiveRecordにはなく、サンプルは配列にあります。api.rubyonrails.org/classes/Array.html#method-i-sample
フランス

3
これは、特に大きなテーブルからランダムなレコードを取得するためのコストのかかる方法です。Railsはテーブルのすべてのレコードのオブジェクトをメモリにロードします。証明が必要な場合は、「rails console」を実行し、「SomeModelFromYourApp.find(:all).sample(5)」を試して、生成されたSQLを確認します。
Eliot Sykes 2013

1
この高価な答えを、複数のランダムなレコードを取得するための合理化された美しさに変える、私の答えをご覧ください。
Arcolye 2013


1

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

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ユーザーしかありません。レコード数が多いほど、パフォーマンスの差は大きくなります。


更新:

550,000行のテーブルで実行

  1. Model.where(id: Model.pluck(:id).sample(10)) 費用 1384.0ms

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

  1. gem: quick_random_records6.4ms完全に費用のみ

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


-2

テーブルから複数のランダムなレコードを取得する非常に簡単な方法。これにより、2つの安価なクエリが作成されます。

Model.where(id: Model.pluck(:id).sample(3))

「3」は、必要なランダムレコードの数に変更できます。


1
いいえ、Model.pluck(:id).sample(3)パーツは安価ではありません。テーブル内のすべての要素のidフィールドを読み取ります。
Maximiliano Guzman

より高速なデータベースにとらわれない方法はありますか?
Arcolye 2013

-5

私はこの問題に遭遇し、自分のDBからランダムな質問を選択したい小さなアプリケーションを開発しました。私が使用した:

@question1 = Question.where(:lesson_id => params[:lesson_id]).shuffle[1]

そしてそれは私にとってはうまく機能しています。これは小さなアプリケーションなので、大きなDBのパフォーマンスについては説明できません。


ええ、これはすべてのレコードを取得し、それらに対してルビー配列メソッドを使用しているだけです。もちろん、欠点は、すべてのレコードをメモリにロードし、ランダムに並べ替えてから、並べ替えられた配列の2番目の項目を取得することです。大規模なデータセットを扱っている場合、それは間違いなく記憶を独り占めする可能性があります。さておき、最初の要素をつかんでみませんか?(例。shuffle[0]
Andrew

シャッフルする必要があります[0]
マルセロオーストリア
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.