ActiveRecord中的随机记录


151

我需要通过ActiveRecord从表中获取随机记录。我遵循了2006年Jamis Buck的例子。

但是,我也通过Google搜索遇到了另一种方式(由于新用户的限制,无法通过链接添加属性):

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

我很好奇这里的其他人是如何做到的,或者是否有人知道哪种方法会更有效。


2
2分可能有助于回答。1.您的ID分布均匀,它们是连续的吗?2.它需要多大的随机性?随机性好还是真正随机性好?
迈克尔

它们是由activerecord自动生成的顺序ID,它必须足够好。
jyunderwood'5

1
然后,您提出的解决方案将接近理想状态:)我将使用“ SELECT table(name)中的MAX(id)”而不是COUNT(*),因为它可以更好地处理已删除的行,否则,其余的都很好。简而言之,如果“足够好”就可以了,那么您只需要拥有一种假设分布接近您实际拥有的方法的方法即可。如果它是统一的,甚至如您所说,那么简单的rand也很有效。
迈克尔

1
当您删除行时,这将不起作用。
Venkat D.

Answers:


136

没有至少两个查询,我还没有找到一种理想的方法。

以下使用随机生成的数字(直到当前记录数)作为偏移量

offset = rand(Model.count)

# Rails 4
rand_record = Model.offset(offset).first

# Rails 3
rand_record = Model.first(:offset => offset)

老实说,我一直在使用ORDER BY RAND()或RANDOM()(取决于数据库)。如果您没有性能问题,那么这不是性能问题。


2
该代码Model.find(:offset => offset).first将引发错误。我认为Model.first(:offset => offset)可能会更好。
Harish Shetty

1
是的,我一直在使用Rails 3,并一直对版本之间的查询格式感到困惑。
Toby Hede,2010年

7
请注意,对于大数据集,使用偏移非常慢,因为它实际上需要索引扫描(或表扫描,以防在使用聚集索引(如InnoDB)的情况下)。换句话说,它是O(N)操作,但是“ WHERE id> =#{rand_id} ORDER BY id ASC LIMIT 1”是O(log N),这要快得多。
肯恩2011年

15
请注意,偏移量方法仅产生一个随机找到的数据点(第一个,其后所有仍按id排序)。如果需要多个随机选择的记录,则必须多次使用此方法或使用数据库提供的随机顺序方法,即Thing.order("RANDOM()").limit(100)100个随机选择的条目。(请注意,它存在RANDOM()于PostgreSQL和RAND()MySQL中……不如您希望的那样可移植。)
Florian Pilz

3
在Rails 4上对我不起作用Model.offset(offset).first。使用。
mahemoff 2014年

206

导轨6

如Jason在评论中所述,在Rails 6中,不允许使用非属性参数。您必须将值包装在Arel.sql()语句中。

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

导轨5、4

Rails 45中,使用PostgresqlSQLite,使用RANDOM()

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

大概同样适用于MySQLRAND()

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

这比公认答案中的方法约2.5倍

警告:对于具有数百万条记录的大型数据集,这很慢,因此您可能需要添加一个limit子句。


4
“ Random()”也可在sqlite中工作,因此对于仍在sqlite上开发并在生产中运行postgres的我们来说,您的解决方案在两种环境中均可工作。
wuliwong 2014年

5
我针对已接受的答案为此创建了一个基准。在Postgresql 9.4上,此答案的处理速度大约是以前的两倍。
panmari 2015年


这是最快的解决方案
Sergio Belevskij

1
“在Rails 6.0中将禁止使用非属性参数。不应使用用户提供的值(例如请求参数或模型属性)来调用此方法。可以通过将已知的安全值包装在Arel.sql()中来传递该值。”
特伦顿·泰勒

73

删除记录后,您的示例代码将开始出现不正确的行为(它将不公平地偏向ID较低的项目)

您最好在数据库中使用随机方法。这些取决于您所使用的数据库,但是:order =>“ RAND()”适用于mysql,而:order =>“ RANDOM()”适用于postgres

Model.first(:order => "RANDOM()") # postgres example

7
随着数据的增加,MySQL的ORDER BY RAND()最终以可怕的运行时结束。即使仅从数千行开始,它也是无法维护的(取决于时间要求)。
迈克尔

Michael提出了一个很好的观点(其他DB也是如此)。通常,从大型表中选择随机行不是您要在动态操作中执行的操作。缓存是您的朋友。重新思考您要实现的目标也不是一个坏主意。
语义

1
在mysql上对大约一百万行的表订购RAND()是slooooooooooooooooooooow。
Subimage

24
不再工作了。使用Model.order("RANDOM()").first代替。
phil pirozhkov

慢和特定于数据库。ActiveRecord应该可以在数据库之间无缝运行,因此您不应使用此方法。
Dex 2013年

29

在具有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表”。那是行不通的。


1
对于那些寻求更多测试的人来说,真正的随机方法需要花费多长时间:我尝试Thing.order("RANDOM()").first了一个包含250k条目的表-查询在不到半秒钟的时间内完成了。(PostgreSQL 9.0,REE 1.8.7、2 x 2.66 GHz内核)对于我来说这已经足够快了,因为我正在执行一次“清理”。
Florian Pilz

6
Ruby的rand方法返回的数字比指定的数字少一个,因此您将需要它,rand_id = rand(Product.count) + 1或者永远不会获得最后一个记录。
里奇

4
random1如果您删除表中的一行,则注释将不起作用。(计数将小于最大ID,您将永远无法选择具有高ID的行)。
尼古拉斯

使用random2可以由能够提高#order使用一个索引列。
卡森·赖因克

18

不必那么难。

ids = Model.pluck(:id)
random_model = Model.find(ids.sample)

pluck返回表中所有ID的数组。在sample该阵列上的方法,则返回从所述阵列的随机ID。

这应该表现良好,并且选择和支持删除行的表的可能性均等。您甚至可以将其与约束条件混合使用。

User.where(favorite_day: "Friday").pluck(:id)

从而选择一个喜欢星期五的随机用户,而不是任何用户。


8
这很干净,适用于小桌子或一次性使用,请注意它不会扩展。在3M表上,在MariaDB上,拔出ID大约需要15秒。
mahemoff 2014年

2
那是个很好的观点。您是否找到了一种更快的替代解决方案,同时又保持了相同的质量?
Niels B.

接受的胶印解决方案不保持相同的质量吗?
mahemoff 2014年

不,它不支持条件,并且对于具有已删除记录的表没有相同的选择概率。
Niels B.

1
试想一下,如果您在使用偏移量进行计数和选择时都应用了约束,则该技术应该会起作用。我只是想将其应用到计数上。
Niels B.

15

不建议您使用此解决方案,但是如果出于某些原因您确实想在只查询一个数据库的同时随机选择一条记录,则可以使用Ruby Array类中sample方法,该方法允许您选择一个随机项从数组。

Model.all.sample

此方法仅需要数据库查询,但比诸如Model.offset(rand(Model.count)).first需要两个数据库查询的替代方法要慢得多,尽管后者仍然是首选。


99
不要这样做。曾经
Zabba 2012年

5
如果数据库中有10万行,则所有这些行都必须加载到内存中。
Venkat D.

3
当然,不建议将其用于生产实时代码,但是我喜欢这种解决方案,很明显,可以将其用于特殊情况,例如使用假值为数据库播种
fguillen

13
拜托-永不言败。如果表很小,这对于开发时调试是一个很好的解决方案。(如果您正在取样,则调试很有可能是用例)。
mahemoff

我用来播种,对我有好处。此外,Model.all.sample(n)也可以工作:)
Arnaldo Ignacio GasparVéjar'13

13

我制作了一个rails 3 gem来处理这个问题:

https://github.com/spilliton/randumb

它允许您执行以下操作:

Model.where(:column => "value").random(10)

7
在此gem的文档中,他们解释说“ randumb只是在查询中附加了一个附加项ORDER BY RANDOM()RAND()对于mysql而言)。” –因此,使用此gem时,@ semanticart的答案注释中提到的不良性能注释也适用。但是至少它是独立于数据库的。
Nicolas 2014年

8

我经常在控制台中使用它,因此我在初始化器中扩展了ActiveRecord-Rails 4示例:

class ActiveRecord::Base
  def self.random
    self.limit(1).offset(rand(self.count)).first
  end
end

然后,我可以打电话Foo.random找回随机记录。


1
需要limit(1)吗?ActiveRecord#first应该足够聪明地做到这一点。
tokland 2014年

6

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)

1
无需-1,兰特最多可以达到num-1
anemaria20 '16

谢谢,更改了:+1:
Thomas Klemm

5

读完所有这些内容后,我对使用Rails 5和MySQL / Maria 5.5在我的特定情况下哪种方法最有效没有太多的信心。因此,我在大约65000条记录上测试了一些答案,并得出以下两个结论:

  1. RAND()与a limit无疑是赢家。
  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)

该答案综合,验证和更新了Mohamed的答案,以及Nami WANG对此的评论和Florian Pilz对该接受的答案的评论- 请发送投票!


3

您可以使用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">

我不建议在使用AR时使用数组方法。这种方式几乎花费了order('rand()').limit(1)完成“相同”工作的时间的8倍(记录了约10K)。
塞巴斯蒂安·帕尔玛

3

强烈建议将此gem用于随机记录,它是专为具有大量数据行的表而设计的:

https://github.com/haopingfan/quick_random_records

除以下gem之外,所有其他答案在大型数据库上的表现都很差:

  1. quick_random_records仅花费4.6ms全部。

在此处输入图片说明

  1. User.order('RAND()').limit(10)成本733.0ms

在此处输入图片说明

  1. 公认的答案offset方法总成本245.4ms

在此处输入图片说明

  1. User.all.sample(10)方法成本573.4ms

在此处输入图片说明


注意:我的表只有120,000个用户。您拥有的记录越多,性能差异就越大。


2

如果需要在指定范围内选择一些随机结果

scope :male_names, -> { where(sex: 'm') }
number_of_results = 10

rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)

1

从列表中随机选择项目的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 }

如果已经缓存了模型的大小,这将是一个查询,否则将是两个查询。


1

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

您也可以使用postgres order('random()'和MySQL order('rand()')。这绝对是最好的答案。
jrochkind


1

如果您使用的是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撰写2ndQuadrant博客文章


1

在看到如此多的答案之后,我决定将它们全部以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)

1

很老的问题,但有:

rand_record = Model.all.shuffle

您得到了一个记录数组,按随机顺序排序。不需要宝石或脚本。

如果您想要一个记录:

rand_record = Model.all.shuffle.first

1
这不是最佳选择,因为这会将所有记录加载到内存中。此外,shuffle.first==.sample
安德鲁·罗任科

0

我是RoR的新手,但我可以为我工作:

 def random
    @cards = Card.all.sort_by { rand }
 end

来自:

如何在Ruby中随机排序(加扰)数组?


4
坏的是它将从数据库中加载所有卡。在数据库中执行此操作效率更高。
安东·库兹明

您也可以使用来随机排列数组array.shuffle。无论如何,要当心,因为Card.all会将所有卡记录都加载到内存中,我们正在谈论的对象越多,效率就越低。
Thomas Klemm

0

怎么办:

rand_record = Model.find(Model.pluck(:id).sample)

对我来说很清楚


0

我在我的应用程序上使用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)

0

.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中的缓存以使这些基准可靠。


0

除了使用之外RANDOM(),您还可以将其放入范围内:

class Thing
  scope :random, -> (limit = 1) {
    order('RANDOM()').
    limit(limit)
  }
end

或者,如果您不希望将它用作作用域,则将其放入类方法中。现在Thing.random与一起使用Thing.random(n)


0

根据“随机”的含义和您实际想要做什么, take就足够了。

随机的“含义”是指:

  • 您是说给我任何我不在乎位置的元素吗?那就足够了。
  • 现在,如果您的意思是“给我一个有相当可能的元素,重复实验会给我与集合中不同的元素”,那么,用其他答案中提到的任何一种方法强制“运气”。

例如,对于测试而言,无论如何都可能会随机创建样本数据,因此take绰绰有余,甚至first

https://guides.rubyonrails.org/active_record_querying.html#take

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.