我需要通过 ActiveRecord 从表中获取随机记录。我遵循了 Jamis Buck from 2006 中的示例。
但是,我还通过谷歌搜索遇到了另一种方式(由于新的用户限制,无法使用链接进行归因):
rand_id = rand(Model.count)
rand_record = Model.first(:conditions => ["id >= ?", rand_id])
我很好奇这里的其他人是如何做到的,或者是否有人知道哪种方式更有效。
导轨 6
正如 Jason 在评论中所说,在 Rails 6 中,non-attribute arguments 是不允许的。您必须将值包装在 Arel.sql()
语句中。
Model.order(Arel.sql('RANDOM()')).first
导轨 5、4
在 Rails 4 和 5 中,使用 Postgresql 或 SQLite,使用 RANDOM()
:
Model.order('RANDOM()').first
大概同样适用于带有 RAND()
的 MySQL
Model.order('RAND()').first
这比 accepted answer 中的方法is about 2.5 times更快。
警告:对于具有数百万条记录的大型数据集,这会很慢,因此您可能需要添加 limit
子句。
如果没有至少两个查询,我还没有找到一种理想的方法。
下面使用一个随机生成的数字(直到当前记录计数)作为偏移量。
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 个随机选择的条目。 (请注意,它在 PostgreSQL 中是 RANDOM()
,在 MySQL 中是 RAND()
...不像您希望的那样可移植。)
Model.offset(offset).first
。
一旦删除记录,您的示例代码将开始表现不准确(它将不公平地偏爱具有较低 ID 的项目)
您最好使用数据库中的随机方法。这些取决于您使用的数据库,但 :order => "RAND()" 适用于 mysql, :order => "RANDOM()" 适用于 postgres
Model.first(:order => "RANDOM()") # postgres example
Model.order("RANDOM()").first
。
在 MySQL 5.1.49、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 中的偏移量似乎要慢得多。
编辑我也试过
Product.first(:order => "RAND()")
但是我不得不在大约 60 秒后杀死它。 MySQL 是“复制到磁盘上的 tmp 表”。那是行不通的。
Thing.order("RANDOM()").first
- 查询在半秒内完成。 (PostgreSQL 9.0,REE 1.8.7,2 x 2.66 GHz 内核)这对我来说已经足够快了,因为我正在进行一次性“清理”。
rand_id = rand(Product.count) + 1
,否则您将永远无法获得最后一条记录。
random1
将不起作用。 (计数将小于最大 ID,您将永远无法选择具有高 ID 的行)。
#order
可以改进使用 random2
。
不建议您使用此解决方案,但如果出于某种原因您真的想要随机选择一条记录,同时只进行一次数据库查询,您可以使用 Ruby Array class 中的 sample
方法,它允许您从数组中选择一个随机项目。
Model.all.sample
此方法只需要一个数据库查询,但它比 Model.offset(rand(Model.count)).first
等需要两个数据库查询的替代方法慢得多,尽管后者仍然是首选。
不必那么难。
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)
ORDER BY RANDOM()
(或 mysql 的 RAND()
)。” – 因此,在对@semanticart 的回答在使用这个 gem 时也适用。但至少它是独立于数据库的。
我经常从控制台使用它,我在初始化程序中扩展 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
应该足够聪明才能做到这一点。
ApplicationRecord
中作为另一种选择。
阅读所有这些内容并没有让我对在我使用 Rails 5 和 MySQL/Maria 5.5 的特定情况下哪一个最适合我充满信心。所以我在大约 65000 条记录上测试了一些答案,并且有两个要点:
有限制的 RAND() 显然是赢家。不要使用采摘+样品。
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 对已接受答案的评论 - 请给他们投票!
强烈推荐这个 gem 用于随机记录,它是专门为具有大量数据行的表设计的:
https://github.com/haopingfan/quick_random_records
所有其他答案在大型数据库中表现不佳,除了这个 gem:
quick_random_records 总共只花费 4.6 毫秒。
https://i.stack.imgur.com/nmydu.png
User.order('RAND()').limit(10) 花费 733.0ms。
https://i.stack.imgur.com/PrFx9.png
接受的答案偏移方法总共花费 245.4 毫秒。
https://i.stack.imgur.com/0iAQe.png
User.all.sample(10) 方法花费 573.4 毫秒。
https://i.stack.imgur.com/8ktRv.png
注意:我的表只有 120,000 个用户。您拥有的记录越多,性能差异就越大。
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)
您可以使用 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)
执行“相同”工作(大约 10K 记录)时间的 8 倍。
如果需要在指定范围内选择一些随机结果:
scope :male_names, -> { where(sex: 'm') }
number_of_results = 10
rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)
非常老的问题,但有:
rand_record = Model.all.shuffle
你有一个记录数组,按随机顺序排序。不需要宝石或脚本。
如果你想要一个记录:
rand_record = Model.all.shuffle.first
shuffle.first
== .sample
从列表中随机挑选项目的 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
order('random()'
的 postgres 和带有 order('rand()')
的 MySQL 来做到这一点。这绝对是最好的答案。
对于 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
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
采样方法是完全随机的,还是只返回随机页面的第一行。
大部分信息取自 2ndQuadrant blog post written by Gulcin Yildirim。
在看到这么多答案后,我决定在我的 PostgreSQL(9.6.3) 数据库上对它们进行基准测试。我使用了一个较小的 100,000 表并摆脱了 Model.order("RANDOM()").first 因为它已经慢了两个数量级。
使用一个有 2,500,000 个条目和 10 列的表格,获胜者是 pluck 方法,比亚军快 8 倍(偏移量。我只在本地服务器上运行这个,所以这个数字可能会被夸大,但它足够大,pluck方法是我最终将使用的方法。还值得注意的是,这可能会导致问题是您一次提取超过 1 个结果,因为每个结果都是唯一的,即不那么随机。
Pluck 在我的 25,000,000 行表上运行 100 次 编辑:实际上这一次包括循环中的 pluck,如果我把它拿出来,它的运行速度与 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
它来自:
How to randomly sort (scramble) an array in Ruby?
array.shuffle
随机播放数组。无论如何,请注意,因为 Card.all
会将所有卡片记录加载到内存中,我们谈论的对象越多,效率就越低。
要做什么:
rand_record = Model.find(Model.pluck(:id).sample)
对我来说很清楚
我在我的应用程序上使用 Benchmark 的 rails 4.2.8 尝试了 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 中),因为由于 DB 缓存,第二次运行的一些数据库查询可能比第一次运行快得多。不幸的是,没有简单的方法可以在 Postgres 中禁用缓存以使这些基准测试可靠。
除了使用 RANDOM()
,您还可以将其放入范围:
class Thing
scope :random, -> (limit = 1) {
order('RANDOM()').
limit(limit)
}
end
或者,如果您不喜欢将其作为范围,只需将其放入类方法中即可。现在 Thing.random
与 Thing.random(n)
一起工作。
如果您想在您选择的数据库上运行基准测试,这里有一个模板:
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
根据“随机”的含义和您实际想要做的事情,take
可能就足够了。
随机的“含义”是指:
你的意思是给我任何我不在乎它的位置的元素吗?那么这就足够了。
现在,如果您的意思是“给我任何具有合理概率的元素,重复实验会给我集合中的不同元素”,那么请使用其他答案中提到的任何方法强制“运气”。
例如,对于测试,样本数据本来可以随机创建,所以 take
绰绰有余,老实说,甚至 first
。
https://guides.rubyonrails.org/active_record_querying.html#take
Model.except(:order).order(Arel.sql('RANDOM()')).first
如果您有一个覆盖顺序的默认范围