ActiveRecord 中的随机记录

我需要通过 ActiveRecord 从表中获取随机记录。

然而,我也通过谷歌搜索找到了另一种方式(由于新的用户限制,无法给链接加上属性) :

 rand_id = rand(Model.count)
rand_record = Model.first(:conditions => ["id >= ?", rand_id])

我很好奇这里的其他人是怎么做到的或者是否有人知道哪种方式更有效率。

111440 次浏览

一旦记录被删除,示例代码的行为将开始不准确(它将不公平地偏爱 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方法,该方法允许您从数组中选择一个随机项。

Model.all.sample

这种方法只需要一个数据库查询,但是比需要两个数据库查询的 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。

这应该能够很好地执行,并且具有相同的选择概率,并且支持具有删除行的表。您甚至可以将其与约束混合。

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带回一个随机记录。

铁路6

正如 Jason 在评论中所说,在 Rails 6中,非属性参数是不允许的。必须将该值包装在 Arel.sql()语句中。

Model.order(Arel.sql('RANDOM()')).first

铁路5号,4号

铁路45中,使用 事后SQLite,使用 RANDOM():

Model.order('RANDOM()').first

对于 MySQLRAND()大概也是一样的

Model.order('RAND()').first

这种 一个 ref = “ https://gist.github.com/panmari/73a2c203d24e7e9461d1”rel = “ noReferrer”大约快了2.5倍比在 接受的答案的方法。

警告 : 对于有数百万条记录的大型数据集来说,这样做比较慢,因此您可能需要添加一个 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里:

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选择随机记录。

两个默认抽样方法(SYSTEMBERNOULLI)要求您指定要返回的行数占表中总行数的百分比。

-- 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

在看到这么多答案之后,我决定在我的 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个记录中测试了一些答案,得到了两个结论:

  1. 具有 limit的 RAND ()显然是赢家。
  2. 不要使用 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)

这个答案综合、验证和更新了 穆罕默德的回答,以及 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查询,返回一个集合,例如:

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 发生过。在 给你给你中有更多的解释。

对于大型表的一种解决方案是 .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 没有简单的方法可以禁用 cache 来确保这些基准的可靠性。

强烈推荐随机记录使用这个 gem,它是专门为拥有大量数据行的表设计的:

Https://github.com/haopingfan/quick_random_records

所有其他答案在大型数据库中的表现都很糟糕,除了这个 gem:

  1. 总共只花费 4.6ms

enter image description here

  1. User.order('RAND()').limit(10)的价格是 733.0ms

enter image description here

  1. 接受的答案 offset的方法成本 245.4ms完全。

enter image description here

  1. User.all.sample(10)接近成本 573.4ms

enter image description here


注意: 我的表只有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.randomThing.random(n)一起工作。

根据“随机”的含义和您实际想要做的事情,take可能就足够了。

我说的“随机”的意思是:

  • 你的意思是给我任何元素,我不在乎它的位置? 那就足够了。
  • 现在,如果你的意思是“给我任何元素,有一个合理的概率 重复的实验会给我带来不同的元素 然后,强迫“运气”与任何方法提到的其他 答案。

例如,对于测试来说,样本数据本来就可以随机创建,所以 take就足够了,老实说,甚至 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

在 id 均匀分布的情况下,这将更随机地工作