我需要通过ActiveRecord从表中获取随机记录。我遵循了2006年Jamis Buck的例子。
但是,我也通过Google搜索遇到了另一种方式(由于新用户的限制,无法通过链接添加属性):
rand_id = rand(Model.count)
rand_record = Model.first(:conditions => ["id >= ?", rand_id])
我很好奇这里的其他人是如何做到的,或者是否有人知道哪种方法会更有效。
我需要通过ActiveRecord从表中获取随机记录。我遵循了2006年Jamis Buck的例子。
但是,我也通过Google搜索遇到了另一种方式(由于新用户的限制,无法通过链接添加属性):
rand_id = rand(Model.count)
rand_record = Model.first(:conditions => ["id >= ?", rand_id])
我很好奇这里的其他人是如何做到的,或者是否有人知道哪种方法会更有效。
Answers:
没有至少两个查询,我还没有找到一种理想的方法。
以下使用随机生成的数字(直到当前记录数)作为偏移量。
offset = rand(Model.count)
# Rails 4
rand_record = Model.offset(offset).first
# Rails 3
rand_record = Model.first(:offset => offset)
老实说,我一直在使用ORDER BY RAND()或RANDOM()(取决于数据库)。如果您没有性能问题,那么这不是性能问题。
Model.find(:offset => offset).first
将引发错误。我认为Model.first(:offset => offset)
可能会更好。
Thing.order("RANDOM()").limit(100)
100个随机选择的条目。(请注意,它存在RANDOM()
于PostgreSQL和RAND()
MySQL中……不如您希望的那样可移植。)
Model.offset(offset).first
。使用。
如Jason在评论中所述,在Rails 6中,不允许使用非属性参数。您必须将值包装在Arel.sql()
语句中。
Model.order(Arel.sql('RANDOM()')).first
在Rails 4和5中,使用Postgresql或SQLite,使用RANDOM()
:
Model.order('RANDOM()').first
大概同样适用于MySQL与RAND()
Model.order('RAND()').first
警告:对于具有数百万条记录的大型数据集,这很慢,因此您可能需要添加一个limit
子句。
删除记录后,您的示例代码将开始出现不正确的行为(它将不公平地偏向ID较低的项目)
您最好在数据库中使用随机方法。这些取决于您所使用的数据库,但是:order =>“ RAND()”适用于mysql,而:order =>“ RANDOM()”适用于postgres
Model.first(:order => "RANDOM()") # postgres example
Model.order("RANDOM()").first
代替。
在具有500万条记录的product表上将这两种方法在MySQL 5.1.49和Ruby 1.9.2p180上进行基准测试:
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表”。那是行不通的。
Thing.order("RANDOM()").first
了一个包含250k条目的表-查询在不到半秒钟的时间内完成了。(PostgreSQL 9.0,REE 1.8.7、2 x 2.66 GHz内核)对于我来说这已经足够快了,因为我正在执行一次“清理”。
rand_id = rand(Product.count) + 1
或者永远不会获得最后一个记录。
random1
如果您删除表中的一行,则注释将不起作用。(计数将小于最大ID,您将永远无法选择具有高ID的行)。
random2
可以由能够提高#order
使用一个索引列。
不必那么难。
ids = Model.pluck(:id)
random_model = Model.find(ids.sample)
pluck
返回表中所有ID的数组。在sample
该阵列上的方法,则返回从所述阵列的随机ID。
这应该表现良好,并且选择和支持删除行的表的可能性均等。您甚至可以将其与约束条件混合使用。
User.where(favorite_day: "Friday").pluck(:id)
从而选择一个喜欢星期五的随机用户,而不是任何用户。
不建议您使用此解决方案,但是如果出于某些原因您确实想在只查询一个数据库的同时随机选择一条记录,则可以使用Ruby Array类中的sample
方法,该方法允许您选择一个随机项从数组。
Model.all.sample
此方法仅需要数据库查询,但比诸如Model.offset(rand(Model.count)).first
需要两个数据库查询的替代方法要慢得多,尽管后者仍然是首选。
我制作了一个rails 3 gem来处理这个问题:
https://github.com/spilliton/randumb
它允许您执行以下操作:
Model.where(:column => "value").random(10)
ORDER BY RANDOM()
(RAND()
对于mysql而言)。” –因此,使用此gem时,@ semanticart的答案注释中提到的不良性能注释也适用。但是至少它是独立于数据库的。
我经常在控制台中使用它,因此我在初始化器中扩展了ActiveRecord-Rails 4示例:
class ActiveRecord::Base
def self.random
self.limit(1).offset(rand(self.count)).first
end
end
然后,我可以打电话Foo.random
找回随机记录。
limit(1)
吗?ActiveRecord#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)
读完所有这些内容后,我对使用Rails 5和MySQL / Maria 5.5在我的特定情况下哪种方法最有效没有太多的信心。因此,我在大约65000条记录上测试了一些答案,并得出以下两个结论:
limit
无疑是赢家。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的答案,以及Nami WANG对此的评论和Florian Pilz对该接受的答案的评论- 请发送投票!
您可以使用Array
方法sample
,该方法sample
从数组中返回一个随机对象,为了使用它,您只需要在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('rand()').limit(1)
完成“相同”工作的时间的8倍(记录了约10K)。
强烈建议将此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个用户。您拥有的记录越多,性能差异就越大。
如果需要在指定范围内选择一些随机结果:
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
。想要sample
为ActiveRecord 创建高效的工具,并根据前面的答案,我使用了:
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 }
如果已经缓存了模型的大小,这将是一个查询,否则将是两个查询。
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
order('random()'
和MySQL order('rand()')
。这绝对是最好的答案。
对于MySQL数据库,请尝试:Model.order(“ RAND()”)。first
如果您使用的是PostgreSQL 9.5+,则可以利用它TABLESAMPLE
来选择随机记录。
两种默认的采样方法(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博客文章。
在看到如此多的答案之后,我决定将它们全部以PostgreSQL(9.6.3)数据库为基准。我使用了一个较小的100,000表,并删除了Model.order(“ RANDOM()”)。首先,因为它已经慢了两个数量级。
使用具有10列的2,500,000个条目的表,不让对手获胜的方法是拔子方法,几乎比亚军的速度快8倍(偏移量。我仅在本地服务器上运行此方法,因此该数字可能会被夸大,但其大小足以使拔人方法是我最终要使用的方法,还值得注意的是,这可能会导致问题,因为您一次选择1个以上的结果,因为每个结果都是唯一的,也就是随机性较低。
Pluck在我的25,000,000行表上赢得了100次运行的胜利编辑:实际上,如果我将其剔除,则这次包括循环中的采摘,它的运行速度与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)
array.shuffle
。无论如何,要当心,因为Card.all
会将所有卡记录都加载到内存中,我们正在谈论的对象越多,效率就越低。
我在我的应用程序上使用Benchmark的Rails 4.2.8尝试了Sam的示例(我将1..Category.count设置为random,因为如果random取为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)
.order('RANDOM()').limit(limit)
看起来很整洁,但是对于大表却很慢,因为即使它limit
是1,它也需要获取所有行并对其进行排序(在数据库中是内部的,在Rails中不是)。我不确定MySQL,但这发生在Postgres中。在这里和这里有更多的解释。
对于大表的一个解决方案是.from("products TABLESAMPLE SYSTEM(0.5)")
,其中0.5
的装置0.5%
。但是,我发现如果您的WHERE
条件会过滤掉许多行,则此解决方案仍然很慢。我想这是因为TABLESAMPLE SYSTEM(0.5)
之前提取了所有行WHERE
条件适用。
大表(但不是很随机)的另一种解决方案是:
products_scope.limit(sample_size).sample(limit)
在那里sample_size
可以100
(但不是否则过大很慢,消耗大量的内存),并limit
可以1
。请注意,尽管这是快速的,但并不是真正随机的,但在sample_size
仅记录。
PS:以上答案的基准测试结果不可靠(至少在Postgres中是不可靠的),因为由于数据库缓存,第二次运行的某些数据库查询可能比第一次运行的查询速度快得多。不幸的是,没有简单的方法可以禁用Postgres中的缓存以使这些基准可靠。
除了使用之外RANDOM()
,您还可以将其放入范围内:
class Thing
scope :random, -> (limit = 1) {
order('RANDOM()').
limit(limit)
}
end
或者,如果您不希望将它用作作用域,则将其放入类方法中。现在Thing.random
与一起使用Thing.random(n)
。
根据“随机”的含义和您实际想要做什么, take
就足够了。
随机的“含义”是指:
例如,对于测试而言,无论如何都可能会随机创建样本数据,因此take
绰绰有余,甚至first
。
https://guides.rubyonrails.org/active_record_querying.html#take