【问题标题】:Rails 3: Get Random RecordRails 3:获取随机记录
【发布时间】:2011-07-17 13:39:45
【问题描述】:

所以,我找到了几个在 Rails 2 中查找随机记录的示例——首选方法似乎是:

Thing.find :first, :offset => rand(Thing.count)

作为一个新手,我不确定如何使用 Rails 3 中的新 find 语法来构建它。

那么,查找随机记录的“Rails 3 方式”是什么?

【问题讨论】:

标签: ruby-on-rails ruby ruby-on-rails-3 activerecord random


【解决方案1】:
Thing.first(:order => "RANDOM()") # For MySQL :order => "RAND()", - thanx, @DanSingerman
# Rails 3
Thing.order("RANDOM()").first

Thing.first(:offset => rand(Thing.count))
# Rails 3
Thing.offset(rand(Thing.count)).first

实际上,在 Rails 3 中,所有示例都可以使用。但是使用命令RANDOM 对于大表来说相当慢,但更多的是 sql 风格

UPD。您可以在索引列上使用以下技巧(PostgreSQL 语法):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;

【讨论】:

  • 您的第一个示例在 MySQL 中不起作用 - MySQL 的语法是 Thing.first(:order => "RAND()") (编写 SQL 而不是使用 ActiveRecord 抽象的危险)
  • @DanSingerman,是的,它是特定于 DB 的 RAND()RANDOM()。谢谢
  • 如果索引中缺少项目,这不会产生问题吗? (如果堆栈中间的某些内容被删除,是否有机会请求它?
  • @VictorS,不,它不会#offset 只是转到下一个可用记录。我用 Ruby 1.9.2 和 Rails 3.1 测试了它
  • @JohnMerlino,是的,0 是偏移量,而不是 id。 Offet 0 表示按顺序排列的第一项。
【解决方案2】:

我正在开发一个项目(Rails 3.0.15,ruby 1.9.3-p125-perf),其中 db 位于 localhost 中,并且 users 表有一点超过 10 万条记录

使用

按 RAND() 排序

很慢

User.order("RAND(id)").first

变成

SELECT users.* FROM users ORDER BY RAND(id) LIMIT 1

响应时间为 812 秒!!

Rails 日志:

用户负载 (11030.8ms) SELECT users.* FROM users ORDER BY RAND() 限制 1 个

来自mysql的解释

+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra                           |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 110165 | Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

您可以看到没有使用索引(possible_keys = NULL),创建了一个临时表,并且需要额外的 pass 才能获取所需的值(extra = Using temporary; Using文件排序)。

另一方面,通过将查询分成两部分并使用 Ruby,我们在响应时间上得到了合理的改进。

users = User.scoped.select(:id);nil
User.find( users.first( Random.rand( users.length )).last )

(;nil 用于控制台)

Rails 日志:

用户负载 (25.2ms) SELECT id FROM users 用户负载 (0.2ms) SELECT users.* FROM users WHERE users.id = 106854 限制 1

mysql 的解释证明了原因:

+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| id | select_type | table | type  | possible_keys | key                      | key_len | ref  | rows   | Extra       |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | index | NULL          | index_users_on_user_type | 2       | NULL | 110165 | Using index |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+

+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

我们现在可以只使用索引和主键,完成这项工作的速度提高了大约 500 倍!

更新:

正如icantbecool in cmets所指出的,如果表中有删除记录,上述解决方案存在缺陷。

一种解决方法可以是

users_count = User.count
User.scoped.limit(1).offset(rand(users_count)).first

转化为两个查询

SELECT COUNT(*) FROM `users`
SELECT `users`.* FROM `users` LIMIT 1 OFFSET 148794

并在大约 500 毫秒内运行。

【讨论】:

  • 在第二个示例的“last”之后添加“.id”将避免“找不到没有 ID 的模型”错误。例如。 User.find(users.first(Random.rand(users.length)).last.id)
  • 警告!在 MySQL 中,RAND(id)NOT 为每个查询提供不同的随机顺序。如果每个查询需要不同的顺序,请使用RAND()
  • 如果有记录被删除,User.find(users.first(Random.rand(users.length)).last.id) 将不起作用。 [1,2,4,5,] 并且它可能会选择 3 的 id,但不会有活动的记录关系。
  • 另外,users = User.scoped.select(:id);nil 没有被弃用。改用这个:users = User.where(nil).select(:id)
  • 我相信使用 Random.rand( users.length ) 作为 first 的参数是一个错误。 Random.rand 可以返回 0。当 0 用作 first 的参数时,限制设置为零并且不返回任何记录。应该使用 1 + Random(users.length) 假设 users.length > 0。
【解决方案3】:

如果使用 Postgres

User.limit(5).order("RANDOM()")

如果使用 MySQL

User.limit(5).order("RAND()")

在这两种情况下,您都是从用户表中随机选择 5 条记录。这是控制台中显示的实际 SQL 查询。

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

【讨论】:

    【解决方案4】:

    我为此制作了一个 rails 3 gem,它在大型表上表现更好,并允许您链接关系和范围:

    https://github.com/spilliton/randumb

    (编辑):我的 gem 的默认行为现在基本上使用与上面相同的方法,但是如果您愿意,可以选择使用旧方法:)

    【讨论】:

      【解决方案5】:

      实际上发布的许多答案在相当大的表(1+ 百万行)上表现不佳。随机排序很快需要几秒钟,而在桌子上进行计数也需要很长时间。

      在这种情况下适合我的解决方案是将RANDOM() 与 where 条件一起使用:

      Thing.where('RANDOM() >= 0.9').take
      

      在超过一百万行的表上,此查询通常需要不到 2 毫秒。

      【讨论】:

      • 您的解决方案的另一个优点是使用take 函数,它提供LIMIT(1) 查询但返回单个元素而不是数组。所以我们不需要调用first
      • 在我看来,表格开头的记录以这种方式选择的概率更高,这可能不是您想要实现的。
      【解决方案6】:

      我们来了

      轨道方式

      #in your initializer
      module ActiveRecord
        class Base
          def self.random
            if (c = count) != 0
              find(:first, :offset =>rand(c))
            end
          end
        end
      end
      

      用法

      Model.random #returns single random object
      

      或者第二个想法是

      module ActiveRecord
        class Base
          def self.random
            order("RAND()")
          end
        end
      end
      

      用法:

      Model.random #returns shuffled collection
      

      【讨论】:

      • Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
      • 如果没有任何用户并且您想要获得 2 个用户,那么您会收到错误消息。有道理。
      • 第二种方法不适用于 postgres,但您可以改用 "RANDOM()"...
      【解决方案7】:

      这对我来说非常有用,但是我需要更多的灵活性,所以这就是我所做的:

      案例 1:找到一条随机记录来源:trevor turk 网站
      将此添加到 Thing.rb 模型

      def self.random
          ids = connection.select_all("SELECT id FROM things")
          find(ids[rand(ids.length)]["id"].to_i) unless ids.blank?
      end
      

      然后在你的控制器中你可以调用这样的东西

      @thing = Thing.random
      

      案例 2:找到多个随机记录(不重复)来源:不记得了
      我需要找到 10 个没有重复的随机记录,所以这就是我发现的工作
      在您的控制器中:

      thing_ids = Thing.find( :all, :select => 'id' ).map( &:id )
      @things = Thing.find( (1..10).map { thing_ids.delete_at( thing_ids.size * rand ) } )
      

      这将找到10条随机记录,但值得一提的是,如果数据库特别大(数百万条记录),这将不理想,并且会影响性​​能。 Is 将表现良好,最多可以记录几千条记录,这对我来说已经足够了。

      【讨论】:

        【解决方案8】:

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

        【讨论】:

        • 实际上,#count 将调用数据库以获取COUNT。如果记录已经加载,这可能是个坏主意。重构将改用#size,因为它将决定是否应使用#count,或者,如果记录已加载,则使用#length
        • 根据您的反馈从count 切换到size。更多信息:dev.mensfeld.pl/2014/09/…
        【解决方案9】:

        在 Rails 5 中工作并且与 DB 无关:

        这在你的控制器中:

        @quotes = Quote.offset(rand(Quote.count - 3)).limit(3)
        

        当然,您可以将其放在关注点中,如 here 所示。

        app/models/concerns/randomable.rb

        module Randomable
          extend ActiveSupport::Concern
        
          class_methods do
            def random(the_count = 1)
              records = offset(rand(count - the_count)).limit(the_count)
              the_count == 1 ? records.first : records
            end
          end
        end
        

        那么……

        app/models/book.rb

        class Book < ActiveRecord::Base
          include Randomable
        end
        

        然后你可以简单地使用:

        Books.random
        

        Books.random(3)
        

        【讨论】:

        • 这总是需要后续记录,至少需要记录下来(因为它可能不是用户想要的)。
        【解决方案10】:

        您可以在 ActiveRecord 中使用 sample()

        例如

        def get_random_things_for_home_page
          find(:all).sample(5)
        end
        

        来源:http://thinkingeek.com/2011/07/04/easily-select-random-records-rails/

        【讨论】:

        • 如果你有大量记录,这是一个非常糟糕的查询,因为数据库会选择所有记录,然后 Rails 会从中选择五个记录 - 非常浪费。
        • sample 不在 ActiveRecord 中,样本在数组中。 api.rubyonrails.org/classes/Array.html#method-i-sample
        • 这是一种获取随机记录的昂贵方法,尤其是从大表中。 Rails 会将表中每条记录的对象加载到内存中。如果需要证明,请运行“rails 控制台”,尝试“SomeModelFromYourApp.find(:all).sample(5)”并查看生成的 SQL。
        • 查看我的答案,它将这个昂贵的答案变成了获取多个随机记录的流线型美感。
        【解决方案11】:

        如果使用 Oracle

        User.limit(10).order("DBMS_RANDOM.VALUE")
        

        输出

        SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
        

        【讨论】:

          【解决方案12】:

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


          更新:

          在有 550,000 行的表上执行

          1. Model.where(id: Model.pluck(:id).sample(10)) 费用1384.0ms

          1. gem: quick_random_records 仅花费 6.4ms 全部

          【讨论】:

            【解决方案13】:

            从表中获取多个随机记录的一种非常简单的方法。这会产生 2 个便宜的查询。

            Model.where(id: Model.pluck(:id).sample(3))

            您可以将“3”更改为您想要的随机记录数。

            【讨论】:

            • 不,Model.pluck(:id).sample(3) 部分并不便宜。它将读取表中每个元素的 id 字段。
            • 有更快的与数据库无关的方法吗?
            【解决方案14】:

            我刚刚在开发一个小型应用程序时遇到了这个问题,我想从我的数据库中选择一个随机问题。我用过:

            @question1 = Question.where(:lesson_id => params[:lesson_id]).shuffle[1]
            

            它对我来说效果很好。我无法谈论大型数据库的性能如何,因为这只是一个小型应用程序。

            【讨论】:

            • 是的,这只是获取所有记录并在它们上使用 ruby​​ 数组方法。缺点当然是它意味着将所有记录加载到内存中,然后随机重新排序它们,然后抓取重新排序数组中的第二项。如果您正在处理大型数据集,那肯定会占用大量内存。撇开次要不谈,为什么不抓住第一个元素呢? (即shuffle[0]
            • 必须是随机播放[0]
            猜你喜欢
            • 1970-01-01
            • 2015-05-05
            • 2018-06-06
            • 1970-01-01
            • 1970-01-01
            • 2011-04-08
            • 2015-03-30
            • 2014-08-04
            • 2015-12-20
            相关资源
            最近更新 更多