ChatGPT解决这个技术问题 Extra ChatGPT

ActiveRecord 中的随机记录

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

但是,我还通过谷歌搜索遇到了另一种方式(由于新的用户限制,无法使用链接进行归因):

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

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

2点可能有助于回答。 1. 你的 id 分布有多均匀,它们是连续的吗? 2. 它需要有多随机?足够好的随机,还是真正的随机?
它们是由 activerecord 自动生成的顺序 ID,它必须足够好。
那么您提出的解决方案接近理想:) 我会使用“SELECT MAX(id) FROM table_name”而不是 COUNT(*),因为它会更好地处理已删除的行,否则,其余的都很好。简而言之,如果“足够好”是可以的,那么你只需要一种方法来假设分布接近你实际拥有的分布。如果它是统一的,甚至如你所说,简单的 rand 效果很好。
当您删除了行时,这将不起作用。

M
Mohamad

导轨 6

正如 Jason 在评论中所说,在 Rails 6 中,non-attribute arguments 是不允许的。您必须将值包装在 Arel.sql() 语句中。

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

导轨 5、4

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

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

大概同样适用于带有 RAND()MySQL

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

这比 accepted answer 中的方法is about 2.5 times更快

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


“Random()” 也适用于 sqlite,因此对于我们这些仍在 sqlite 上开发并在生产中运行 postgres 的人来说,您的解决方案在这两种环境中都适用。
我针对接受的答案为此创建了一个 benchmark。在 Postgresql 9.4 上,这个答案的方法大约是两倍快。
似乎不建议在 mysql webtrenches.com/post.cfm/avoid-rand-in-mysql 上使用
“在 Rails 6.0 中将不允许使用非属性参数。不应使用用户提供的值调用此方法,例如请求参数或模型属性。可以通过将已知安全值包装在 Arel.sql() 中来传递它们。”
Model.except(:order).order(Arel.sql('RANDOM()')).first 如果您有一个覆盖顺序的默认范围
J
Jonathan Allard

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

下面使用一个随机生成的数字(直到当前记录计数)作为偏移量。

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) 可能表现更好。
是的,我一直在使用 Rails 3,并且一直对版本之间的查询格式感到困惑。
请注意,对于大型数据集,使用偏移量非常慢,因为它实际上需要索引扫描(或表扫描,以防像 InnoDB 一样使用聚集索引)。换句话说,它是 O(N) 操作,但“WHERE id >= #{rand_id} ORDER BY id ASC LIMIT 1”是 O(log N),这要快得多。
请注意,偏移方法仅产生一个随机找到的数据点(第一个,之后仍然按 id 排序)。如果您需要多个随机选择的记录,您必须多次使用这种方法或使用您的数据库提供的随机排序方法,即 Thing.order("RANDOM()").limit(100) 用于 100 个随机选择的条目。 (请注意,它在 PostgreSQL 中是 RANDOM(),在 MySQL 中是 RAND() ...不像您希望的那样可移植。)
在 Rails 4 上对我不起作用。使用 Model.offset(offset).first
s
semanticart

一旦删除记录,您的示例代码将开始表现不准确(它将不公平地偏爱具有较低 ID 的项目)

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

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

随着数据的增加,MySQL 的 ORDER BY RAND() 最终会在可怕的运行时结束。即使从数千行开始,它也是不可维护的(取决于时间要求)。
Michael 提出了一个很好的观点(其他数据库也是如此)。通常,从大表中选择随机行并不是您想要在动态操作中执行的操作。缓存是你的朋友。重新考虑你想要完成的事情也可能不是一个坏主意。
在具有大约一百万行的表上的 mysql 中订购 RAND() 是 slooooooooooooooooooooow。
不再工作了。请改用 Model.order("RANDOM()").first
速度慢且特定于数据库。 ActiveRecord 应该在数据库之间无缝工作,所以你不应该使用这种方法。
d
dkam

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


对于那些寻求更多测试的人来说,真正的随机方法需要多长时间:我在一个有 250k 条目的表上尝试了 Thing.order("RANDOM()").first - 查询在半秒内完成。 (PostgreSQL 9.0,REE 1.8.7,2 x 2.66 GHz 内核)这对我来说已经足够快了,因为我正在进行一次性“清理”。
Ruby 的 rand 方法返回比指定数字少一的结果,因此您需要 rand_id = rand(Product.count) + 1 ,否则您将永远无法获得最后一条记录。
如果您曾经删除表中的一行,请注意 random1 将不起作用。 (计数将小于最大 ID,您将永远无法选择具有高 ID 的行)。
使用索引列的 #order 可以改进使用 random2
S
Sajad Torkamani

不建议您使用此解决方案,但如果出于某种原因您真的想要随机选择一条记录,同时只进行一次数据库查询,您可以使用 Ruby Array class 中的 sample 方法,它允许您从数组中选择一个随机项目。

Model.all.sample

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


如果您的数据库中有 100k 行,则所有这些都必须加载到内存中。
当然不推荐用于生产实时代码,但我喜欢这个解决方案,它很明显用于特殊情况,例如用假值播种数据库。
请-永远不要说永远。如果表很小,这对于开发时调试来说是一个很好的解决方案。 (如果您正在采样,调试很可能是用例)。
我用来播种,对我有好处。此外, Model.all.sample(n) 也可以:)
同意@mahemoff,您可能有一个大型复杂表并想要一个结果样本,所以调用类似 Foo.where("json_col -> 'foo' ->> 'bar' ILIKE ?", "%baz%") .sample(5) 将允许您检查结果。
N
Niels B.

不必那么难。

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

pluck 返回表中所有 id 的数组。数组上的 sample 方法从数组中返回一个随机 id。

这应该表现良好,具有相同的选择概率和对已删除行的表的支持。您甚至可以将其与约束混合使用。

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

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


这很干净,适用于小桌子或一次性使用,请注意它不会扩展。在一张 3M 的桌子上,我在 MariaDB 上提取 ID 大约需要 15 秒。
那是个很好的观点。您是否找到了一个更快、同时保持相同质量的替代解决方案?
公认的偏移解决方案不保持相同的质量吗?
不,它不支持条件,并且对于已删除记录的表的选择概率不相等。
想一想,如果您在计算和选择偏移时都应用约束,那么该技术应该可以工作。我想只在计数上应用它。
s
spilliton

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

https://github.com/spilliton/randumb

它允许你做这样的事情:

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

在这个 gem 的文档中,他们解释了 “randumb 只是在您的查询中添加了一个额外的 ORDER BY RANDOM()(或 mysql 的 RAND())。” – 因此,在对@semanticart 的回答在使用这个 gem 时也适用。但至少它是独立于数据库的。
K
Knotty66

我经常从控制台使用它,我在初始化程序中扩展 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 中作为另一种选择。
S
Sam

阅读所有这些内容并没有让我对在我使用 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 对已接受答案的评论 - 请给他们投票!


D
Derek Fan

强烈推荐这个 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 个用户。您拥有的记录越多,性能差异就越大。


T
Thomas Klemm

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,rand 最多计数到 num - 1
谢谢,已更改:+1:
t
trejo08

您可以使用 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) 执行“相同”工作(大约 10K 记录)时间的 8 倍。
Y
Yuri Karpovich

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

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

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

G
Gregdebrick

非常老的问题,但有:

rand_record = Model.all.shuffle

你有一个记录数组,按随机顺序排序。不需要宝石或脚本。

如果你想要一个记录:

rand_record = Model.all.shuffle.first

不是最好的选择,因为这会将所有记录加载到内存中。另外,shuffle.first == .sample
D
Dan Kohn

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

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


m
mahatmanich

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 来做到这一点。这绝对是最好的答案。
V
Vadim Eremeev

对于 MySQL 数据库尝试:Model.order("RAND()").first


这在 mysql 上不起作用 .. 你应该至少知道这个假设可以使用什么数据库引擎
对不起,有错字。现在修好了。应该适用于mysql(仅限)
A
Adam Sheehan

如果您使用的是 PostgreSQL 9.5+,您可以利用 TABLESAMPLE 选择随机记录。

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

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


M
Mendoza

在看到这么多答案后,我决定在我的 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)

C
Community

我是 RoR 的新手,但我得到了这个为我工作:

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

它来自:

How to randomly sort (scramble) an array in Ruby?


它的坏处是它将从数据库中加载所有卡片。在数据库中执行此操作更有效。
您还可以使用 array.shuffle 随机播放数组。无论如何,请注意,因为 Card.all 会将所有卡片记录加载到内存中,我们谈论的对象越多,效率就越低。
p
poramo

要做什么:

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

对我来说很清楚


r
rld

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

L
Linh Dam

.order('RANDOM()').limit(limit) 看起来很简洁,但对于大型表来说速度很慢,因为即使 limit 为 1(在数据库内部但在 Rails 中没有),它也需要获取和排序所有行。我不确定 MySQL,但这发生在 Postgres 中。 herehere 中有更多解释。

大型表的一种解决方案是 .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 中禁用缓存以使这些基准测试可靠。


D
Damien Roche

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

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

或者,如果您不喜欢将其作为范围,只需将其放入类方法中即可。现在 Thing.randomThing.random(n) 一起工作。


D
Dragonn steve

您可以获取所有 id 的数组,然后使用 sample 方法返回随机元素。

Model.ids.sample

D
Dorian

如果您想在您选择的数据库上运行基准测试,这里有一个模板:

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

j
jgomo3

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

随机的“含义”是指:

你的意思是给我任何我不在乎它的位置的元素吗?那么这就足够了。

现在,如果您的意思是“给我任何具有合理概率的元素,重复实验会给我集合中的不同元素”,那么请使用其他答案中提到的任何方法强制“运气”。

例如,对于测试,样本数据本来可以随机创建,所以 take 绰绰有余,老实说,甚至 first

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