我需要通过ActiveRecord从表中获取随机记录。我跟随了Jamis Buck from 2006的例子。
但是,我也通过Google搜索遇到了另一种方式(由于新的用户限制,无法使用链接进行归因):
rand_id = rand(Model.count)
rand_record = Model.first(:conditions => ["id >= ?", rand_id])
我很好奇这里的其他人是如何做到的,或者是否有人知道哪种方式会更有效率。
如果没有至少两个查询,我还没有找到理想的方法。
以下使用随机生成的数字(最多为当前记录计数)作为偏移量。
offset = rand(Model.count)
# Rails 4
rand_record = Model.offset(offset).first
# Rails 3
rand_record = Model.first(:offset => offset)
说实话,我刚刚使用ORDER BY RAND()或RANDOM()(取决于数据库)。如果您没有性能问题,这不是性能问题。
阅读所有这些内容并没有让我对使用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's answer,以及Nami WANG对此的评论以及Florian Pilz对接受答案的评论 - 请向他们发送投票!
您可以使用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">
如果需要在指定范围内选择一些随机结果:
scope :male_names, -> { where(sex: 'm') }
number_of_results = 10
rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)
强烈推荐这个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个用户。您拥有的记录越多,性能差异就越大。
从列表中随机选择项目的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 }
如果模型的大小已经被缓存,则这将是一个查询,否则将是两个查询。
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
对于MySQL数据库,请尝试:Model.order(“RAND()”)
如果你正在使用PostgreSQL 9.5+,你可以利用TABLESAMPLE
来选择随机记录。
两种默认采样方法(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
采样方法是完全随机的还是只是从随机页面返回第一行。
在看到这么多答案后,我决定在PostgreSQL(9.6.3)数据库上对它们进行基准测试。我使用一个较小的100,000表并摆脱了Model.order(“RANDOM()”)。首先,它已经慢了两个数量级。
使用一个包含2,500,000个条目和10列的表格,获胜者是采摘方法,其速度几乎是亚军的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)
我是RoR的新手,但我让这个为我工作:
def random
@cards = Card.all.sort_by { rand }
end
它来自:
在Rails 4和5中,使用Postgresql或SQLite,使用RANDOM()
:
Model.order('RANDOM()').first
据推测,与RAND()
相同的MySQL也适用
Model.order('RAND()').first
这个is about 2.5 times比accepted answer的方法更快。
警告:对于包含数百万条记录的大型数据集,这种情况很慢,因此您可能需要添加limit
子句。
怎么办:
rand_record = Model.find(Model.pluck(:id).sample)
对我来说很清楚
我在我的应用程序上使用rails 4.2.8 of Benchmark尝试Sam的示例(我将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)
.order('RANDOM()').limit(limit)
看起来整洁,但对于大型表来说很慢,因为即使limit
为1(在数据库内部但在Rails中没有),它也需要获取和排序所有行。我不确定MySQL,但这种情况发生在Postgres。 here和here的更多解释。
大型桌子的一个解决方案是.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中的缓存以使这些基准可靠。
很老的问题,但有:
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)
一起工作。
一旦删除记录,您的示例代码将开始表现不正确(它将不公平地支持具有较低ID的项目)
你最好在数据库中使用随机方法。这取决于您使用的DB,但是:order =>“RAND()”适用于mysql,并且:order =>“RANDOM()”适用于postgres
Model.first(:order => "RANDOM()") # postgres example
在具有+ 500万条记录的产品表上对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表”。那不行。
它不一定很难。
ids = Model.pluck(:id)
random_model = Model.find(ids.sample)
pluck
返回表中所有id的数组。数组上的sample
方法从数组中返回一个随机id。
这应该表现良好,具有相同的选择概率和支持已删除行的表。你甚至可以将它与约束混合在一起。
User.where(favorite_day: "Friday").pluck(:id)
从而挑选一个喜欢星期五而不是任何用户的随机用户。
我做了一个rails 3 gem来处理这个问题:
https://github.com/spilliton/randumb
它允许你做这样的事情:
Model.where(:column => "value").random(10)
不建议您使用此解决方案,但如果由于某种原因您真的想在仅进行一次数据库查询时随机选择记录,则可以使用sample
中的Ruby Array class方法,该方法允许您从中选择随机项数组。
Model.all.sample
这种方法只需要数据库查询,但它比Model.offset(rand(Model.count)).first
等需要两个数据库查询的替代方案要慢得多,尽管后者仍然是首选。
我经常在控制台中使用它,我在初始化程序中扩展了ActiveRecord - Rails 4示例:
class ActiveRecord::Base
def self.random
self.limit(1).offset(rand(self.count)).first
end
end
然后我可以打电话给Foo.random
带回一个随机记录。
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)