我需要通过 ActiveRecord 从表中获取随机记录。
然而,我也通过谷歌搜索找到了另一种方式(由于新的用户限制,无法给链接加上属性) :
rand_id = rand(Model.count) rand_record = Model.first(:conditions => ["id >= ?", rand_id])
我很好奇这里的其他人是怎么做到的或者是否有人知道哪种方式更有效率。
一旦记录被删除,示例代码的行为将开始不准确(它将不公平地偏爱 id 较低的项目)
您最好使用数据库中的随机方法。这取决于您使用的数据库,但是: order = > “ RAND ()”适用于 mysql,而: order = > “ RANDOM ()”适用于 postgres
Model.first(:order => "RANDOM()") # postgres example
在没有至少两个查询的情况下,我还没有找到一种理想的方法来完成这项工作。
下面使用随机生成的数字(直到当前记录计数)作为 偏移。
offset = rand(Model.count) # Rails 4 rand_record = Model.offset(offset).first # Rails 3 rand_record = Model.first(:offset => offset)
老实说,我一直在使用 ORDERBYRAND ()或 RANDOM ()(取决于数据库)。如果你没有性能问题,那就不是性能问题。
在 MySQL 5.1.49,Ruby 1.9.2 p180上对这两种方法进行基准测试,在一个有 + 500万条记录的 products 表上:
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 表”。没用的。
我做了一个轨道3宝石来处理这个:
Https://github.com/spilliton/randumb
它允许你做这样的事情:
Model.where(:column => "value").random(10)
建议您不要使用这种解决方案,但是如果出于某种原因,您 真的希望随机选择一条记录,而只进行一次数据库查询,那么您可以使用 Ruby Array 类中的 sample方法,该方法允许您从数组中选择一个随机项。
sample
Model.all.sample
这种方法只需要一个数据库查询,但是比需要两个数据库查询的 Model.offset(rand(Model.count)).first等替代方法要慢得多,尽管后者仍然是首选方法。
Model.offset(rand(Model.count)).first
在 Postgres 有一个问题:
User.order('RANDOM()').limit(3).to_sql # Postgres example => "SELECT "users".* FROM "users" ORDER BY RANDOM() LIMIT 3"
使用偏移量,有两个查询:
offset = rand(User.count) # returns an integer between 0 and (User.count - 1) Model.offset(offset).limit(1)
没必要那么难。
ids = Model.pluck(:id) random_model = Model.find(ids.sample)
pluck返回表中所有 id 的数组。数组上的 sample方法,从数组返回一个随机 ID。
pluck
这应该能够很好地执行,并且具有相同的选择概率,并且支持具有删除行的表。您甚至可以将其与约束混合。
User.where(favorite_day: "Friday").pluck(:id)
因此,随机选择一个喜欢星期五的用户,而不是任何用户。
我是 RoR 的新成员,但是我有这个:
def random @cards = Card.all.sort_by { rand } end
它来自:
如何在 Ruby 中对数组进行随机排序
我经常在控制台中使用它,我在初始化程序中扩展了 ActiveRecord —— Rails 4的例子:
class ActiveRecord::Base def self.random self.limit(1).offset(rand(self.count)).first end end
然后我可以调用 Foo.random带回一个随机记录。
Foo.random
正如 Jason 在评论中所说,在 Rails 6中,非属性参数是不允许的。必须将该值包装在 Arel.sql()语句中。
Arel.sql()
Model.order(Arel.sql('RANDOM()')).first
在 铁路4和 5中,使用 事后或 SQLite,使用 RANDOM():
RANDOM()
Model.order('RANDOM()').first
对于 MySQL和 RAND()大概也是一样的
RAND()
Model.order('RAND()').first
这种 一个 ref = “ https://gist.github.com/panmari/73a2c203d24e7e9461d1”rel = “ noReferrer”大约快了2.5倍比在 接受的答案的方法。
警告 : 对于有数百万条记录的大型数据集来说,这样做比较慢,因此您可能需要添加一个 limit子句。
limit
如需选择 指定范围内的一些随机结果:
scope :male_names, -> { where(sex: 'm') } number_of_results = 10 rand = Names.male_names.pluck(:id).sample(number_of_results) Names.where(id: rand)
从列表中随机选择项的 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里:
lib/ext/sample.rb
config/initializers/monkey_patches.rb
Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }
如果模型的大小已经被缓存,那么这将是一个查询,否则是两个查询。
对于 MySQL 数据库,请尝试: Model.order (“ RAND ()”) . first
Rails 4.2和 Oracle :
对于 Oracle,您可以像下面这样设置 Model 的作用域:
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
如果使用 PostgreSQL 9.5 + ,则可以利用 TABLESAMPLE选择随机记录。
TABLESAMPLE
两个默认抽样方法(SYSTEM和 BERNOULLI)要求您指定要返回的行数占表中总行数的百分比。
SYSTEM
BERNOULLI
-- Fetch 10% of the rows in the customers table. SELECT * FROM customers TABLESAMPLE BERNOULLI(10);
这需要知道表中记录的数量,以选择适当的百分比,这可能不容易快速找到。幸运的是,有一个 tsm_system_rows模块允许您指定要直接返回的行数。
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子句:
from
customer = Customer.from("customers TABLESAMPLE SYSTEM_ROWS(1)").first
我不知道 SYSTEM_ROWS采样方法是完全随机的,还是仅仅从随机页面返回第一行。
SYSTEM_ROWS
这些信息大部分来自 作者 Gulcin Yildirim。
在看到这么多答案之后,我决定在我的 PostgreSQL (9.6.3)数据库中对它们进行基准测试。我使用了一个更小的100,000个表,去掉了 Model.order (“ RANDOM ()”)。第一次是因为它已经慢了两数量级。
使用一个有250万条目和10列的表格,毫无疑问的赢家是 pluck 方法,其速度几乎是亚军的8倍(偏移量)。我只在本地服务器上运行这个函数,因此这个数字可能会被夸大,但是它足够大,因此我最终将使用 pluck 方法。值得注意的是,这可能会导致问题是你采摘超过1个结果,因为每个结果将是唯一的,即较少随机。
Pluck 在我的2500万行表上跑了100次 编辑: 实际上这一次包括了循环中的 pluck,如果我把它取出来,它的运行速度和在 id 上的简单迭代一样快。但是,它确实占用了相当大的内存。
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)
那写作呢:
rand_record = Model.find(Model.pluck(:id).sample)
这就说明了你在做什么。
阅读所有这些内容并没有给我带来很大的信心,不知道在我的特定情况下,哪些内容对于 Rails 5和 MySQL/Maria 5.5最有效。因此,我在65000个记录中测试了一些答案,得到了两个结论:
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)
这个答案综合、验证和更新了 穆罕默德的回答,以及 Nami WANG 对同一答案的评论和 Florian Pilz 对公认答案 请给他们投票!的评论
我在我的 App 上使用 Rails 4.2.8的 Benchmark (我把1。.对于随机类别,我们可以统计一下,因为如果随机类别为0,就会产生一个错误(ActiveRecord: : RecordNotfound: Can’t find Category with‘ 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)
你可以使用 Array方法 sample,方法 sample从数组中返回一个随机对象,为了使用它,你只需要执行一个简单的 ActiveRecord查询,返回一个集合,例如:
Array
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">
.order('RANDOM()').limit(limit)看起来很整洁,但对于大型表来说速度很慢,因为即使 limit是1(在数据库内部,但在 Rails 中不是) ,它也需要获取和排序所有行。我不确定是否使用 MySQL,但这种情况在 Postgres 发生过。在 给你和 给你中有更多的解释。
.order('RANDOM()').limit(limit)
对于大型表的一种解决方案是 .from("products TABLESAMPLE SYSTEM(0.5)"),其中 0.5意味着 0.5%。但是,我发现如果使用过滤掉大量行的 WHERE条件,那么这个解决方案仍然很慢。我想这是因为 TABLESAMPLE SYSTEM(0.5)在应用 WHERE条件之前获取所有行。
.from("products TABLESAMPLE SYSTEM(0.5)")
0.5
0.5%
WHERE
TABLESAMPLE SYSTEM(0.5)
对于大型表(但不是很随机)的另一种解决方案是:
products_scope.limit(sample_size).sample(limit)
其中 sample_size可以是 100(但是不要太大,否则它会很慢并消耗大量内存) ,而 limit可以是 1。请注意,虽然这是快速的,但它不是真正的随机,它只是随机的 sample_size记录。
sample_size
100
1
PS: 基准测试结果在上面的答案是不可靠的(至少在 Postgres) ,因为一些数据库查询运行在第二次可以明显快于运行在第一次,由于数据库缓存。不幸的是,在 Postgres 没有简单的方法可以禁用 cache 来确保这些基准的可靠性。
强烈推荐随机记录使用这个 gem,它是专门为拥有大量数据行的表设计的:
Https://github.com/haopingfan/quick_random_records
所有其他答案在大型数据库中的表现都很糟糕,除了这个 gem:
4.6ms
User.order('RAND()').limit(10)
733.0ms
offset
245.4ms
User.all.sample(10)
573.4ms
注意: 我的表只有120,000个用户。你拥有的记录越多,性能的差异就越大。
这个问题很老了,但是:
rand_record = Model.all.shuffle
有一个数组记录,按随机顺序排序。 不需要宝石或脚本。
如果你想要一张唱片:
rand_record = Model.all.shuffle.first
除了使用 RANDOM()之外,您还可以将其放入一个范围:
class Thing scope :random, -> (limit = 1) { order('RANDOM()'). limit(limit) } end
或者,如果您不想将它作为一个范围,只需将它放到一个类方法中。现在,Thing.random与 Thing.random(n)一起工作。
Thing.random
Thing.random(n)
根据“随机”的含义和您实际想要做的事情,take可能就足够了。
take
我说的“随机”的意思是:
例如,对于测试来说,样本数据本来就可以随机创建,所以 take就足够了,老实说,甚至 first也可以。
first
Https://guides.rubyonrails.org/active_record_querying.html#take
您可以获取所有 id 的数组,然后使用 样本方法返回随机元素。
Model.ids.sample
如果你想在你选择的数据库上运行基准测试,这里有一个模板:
gem 'activerecord', git: 'https://github.com/rails/rails' gem 'sqlite3' gem 'benchmark' require 'active_record' require 'benchmark' ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') ActiveRecord::Schema.define do create_table :users end class User < ActiveRecord::Base def self.sample_random order('RANDOM()').first end def self.sample_pluck_id_sample find(pluck(:id).sample) end def self.sample_all_sample all.sample end def self.sample_offset_rand_count offset(rand(count)).first end end USERS_COUNTS = [1000, 10_000, 100_000, 1_000_000] N = 100 USERS_COUNTS.each do |count| puts "Creating #{count} users" User.insert_all((1..count).map { |id| { id: id } }) Benchmark.bm do |x| x.report("sample_random") { N.times { User.sample_random } } x.report("sample_offset_rand_count") { N.times { User.sample_offset_rand_count } } if count < 10_000 x.report("sample_pluck_id_sample") { N.times { User.sample_pluck_id_sample } } x.report("sample_all_sample") { N.times { User.sample_all_sample } } end end puts "Deleting #{User.count} users" User.delete_all end
如果存在数百万条记录,那么通过 RDBMS 进行随机排序可能会非常昂贵。为了简化这一点,您可以这样限制排序记录的数量(PostgreSQL 语法) :
class ApplicationRecord < ActiveRecord::Base def self.sample where( "id >= TRUNC(RANDOM() * (SELECT MAX(id) FROM #{table_name}) + 1)" ).order(:id).first end end
然后是 User.sample
User.sample
在 id 均匀分布的情况下,这将更随机地工作