Случайная запись в ActiveRecord

Мне нужно получить случайную запись из таблицы через ActiveRecord. Я последовал примеру из Джемис бак с 2006 года.

однако я также столкнулся с другим способом через поиск Google (не могу приписать ссылку из-за новых ограничений пользователя):

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

Мне любопытно, как другие здесь это сделали, или если кто-нибудь знает, какой способ будет более эффективным.

22 ответов


Я не нашел идеального способа сделать это без по крайней мере двух запросов.

следующее использует случайно сгенерированное число (до текущего количества записей) в качестве смещение.

offset = rand(Model.count)

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

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

честно говоря, я просто использовал ORDER BY RAND() или RANDOM() (в зависимости от базы данных). Это не проблема производительности, если у вас нет проблемы с производительностью.


на рельсы 4 и 5, используя Postgresql или SQLite, используя RANDOM():

Model.order("RANDOM()").first

предположительно, то же самое будет работать для в MySQL С RAND()

Model.order("RAND()").first

этой примерно в 2,5 раза быстрее чем подход в принято отвечать.

предостережение: это медленно для больших наборов данных с миллионами записей, так что вы возможно, вы захотите добавить limit предложения.


ваш пример кода начнет вести себя неточно, как только записи будут удалены (это будет несправедливо в пользу элементов с более низкими идентификаторами)

вам, вероятно, лучше использовать случайные методы в вашей базе данных. Они варьируются в зависимости от того, какую БД вы используете, но: order = > " RAND () "работает для mysql и :order = >" RANDOM () " работает для postgres

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

бенчмаркинг этих двух методов на MySQL 5.1.49, Ruby 1.9.2p180 на таблице продуктов с записями +5million:

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 на диске". Это не сработает.


это не должно быть так сложно.

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

pluck возвращает массив всех идентификаторов в таблице. The sample метод на массиве, возвращает случайный идентификатор из массива.

это должно работать хорошо, с равной вероятностью выбора и поддержки таблиц с удаленными строками. Вы даже можете смешать его с ограничениями.

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

и тем самым выбрать случайный пользователь, который любит пятницы, а не просто любой пользователь.


Я сделал rails 3 gem, чтобы справиться с этим:

https://github.com/spilliton/randumb

это позволяет вам делать такие вещи:

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

не рекомендуется использовать это решение, но если по какой-то причине вы действительно хотите случайным образом выбрать запись, делая только один запрос базы данных, вы можете использовать sample метод Ruby Array class, который позволяет выбрать случайный элемент из массива.

Model.all.sample

этот метод требует только запроса базы данных, но он значительно медленнее, чем альтернативы, такие как Model.offset(rand(Model.count)).first, который требует двух запросов к базе данных, хотя последний по-прежнему популярны.


Я использую это так часто из консоли, я расширяю ActiveRecord в Примере инициализатора-Rails 4:

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

Я могу позвонить Foo.random чтобы вернуть случайную запись.


один запрос в 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)

чтение всего этого не дало мне большой уверенности в том, какой из них будет работать лучше всего в моей конкретной ситуации с Rails 5 и MySQL/Maria 5.5. Поэтому я проверил некоторые ответы на ~ 65000 записей, и у меня есть два варианта:

  1. RAND () с 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)

этот ответ синтезирует, проверяет и обновляет Мохамед ответ, а также комментарий нами Ванга к тому же и комментарий Флориана Пилца к принятому ответу - пожалуйста, отправьте им голоса!


Если вам нужно выбрать некоторые случайные результаты в пределах заданной области:

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

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

можно использовать 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">

метод 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 }

это будет один запрос, если размер модели уже кэширован и два в противном случае.


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

для базы данных MySQL попробуйте: модель.заказ ("RAND ()").первый


если вы используете PostgreSQL 9.5+, вы можете воспользоваться TABLESAMPLE для выбора случайной записи.

два метода выборки по умолчанию (SYSTEM и BERNOULLI) необходимо указать количество возвращаемых строк в процентах от общего числа строк в таблице.

-- Fetch 10% of the rows in the customers table.
SELECT * FROM customers TABLESAMPLE BERNOULLI(10);

это требует знания количества записей в таблице, чтобы выбрать соответствующий процент, который может быть нелегко найти быстро. К счастью, есть the 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 метод выборки будет полностью случайным или если он просто возвращает первую строку со случайной страницы.

большая часть этой информации была взята из 2ndQuadrant блоге написал Гюльчин Йылдырым.


увидев так много ответов, я решил проверить их все в своей базе данных PostgreSQL(9.6.3). Я использую меньшую таблицу 100,000 и избавился от модели.order("RANDOM()").во-первых, потому что он уже был на два порядка медленнее.

используя таблицу с 2,500,000 записей с 10 столбцами руки вниз победитель был метод отваги, будучи почти в 8 раз быстрее, чем бегун вверх (смещение. Я только запустил это на локальном сервере, чтобы число могло быть завышено, но его достаточно что метод отваги-это то, что я в конечном итоге использую. Также стоит отметить, что это может вызвать проблемы, вы срываете более 1 результата за раз, так как каждый из них будет уникальным ака менее случайным.

Pluck выигрывает 100 раз на моей таблице строк 25,000,000 Edit: на самом деле на этот раз включает в себя рывок в цикле, если я его вытащу, он работает так же быстро, как простая итерация на 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)

вот данных 2000 раз на моей таблице строк 100,000, чтобы исключить random

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)

настоятельно рекомендуем этот камень для случайных записей, который специально разработан для таблицы с большим количеством строк данных:

https://github.com/haopingfan/quick_random_records

все остальные ответы плохо работают с большой базой данных, кроме этого драгоценного камня:

  1. quick_random_records только стоимостью 4.6ms полностью.

enter image description here

  1. the User.order('RAND()').limit(10) стоимость 733.0ms.

enter image description here

  1. принято отвечать offset затратный подход 245.4ms полностью.

enter image description here

  1. the User.all.sample(10) затратный подход 573.4ms.

enter image description here


Примечание: моя таблица имеет только 120 000 пользователей. Чем больше у вас записей, тем больше будет разница в производительности.


Я совершенно новый для RoR, но я получил это, чтобы работать для меня:

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

это пришло от:

как случайным образом сортировать (скремблировать) массив в Ruby?


Что делать:

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

для меня это очень ясно


Я пробую этот пример Сэма в своем приложении, используя rails 4.2.8 Benchmark( я поставил 1..Категория.считайте случайным, потому что если случайный принимает 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. Больше объяснений в здесь и здесь.

одним из решений для больших таблиц является .from("products TABLESAMPLE SYSTEM(0.5)") здесь 0.5 означает 0.5%. Однако я считаю, что это решение все еще медленно, если у вас есть WHERE условия, которые отфильтровывают много строк. Я думаю, это потому, что TABLESAMPLE SYSTEM(0.5) fetch все строки перед WHERE условия применения.

другое решение для больших таблиц (но не очень случайных) составляет:

products_scope.limit(sample_size).sample(limit)

здесь sample_size может быть 100 (но не слишком большой, иначе он медленный и потребляет много памяти), и limit может быть 1. Обратите внимание, что хотя это быстро, но это не совсем случайно, это случайно в