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


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

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

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

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

22 138

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, кажется, намного медленнее.

EDIT Я тоже пробовал

Product.first(:order => "RAND()")

но я должен был убить его через ~60 секунд. MySQL был "копирование в таблицу tmp на диске". Это не сработает.

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

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

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

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

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

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

Я сделал рельсы 3 драгоценный камень, чтобы справиться с этим:

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 возвращает случайный объект из массива, для того, чтобы использовать его, вам просто нужно exec в простой 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);

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

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

Pluck выигрывает 100 раз на моей таблице строк 25,000,000 Edit: на самом деле это время включает в себя выщипывание в цикле, если я его вытащу, он работает примерно так же быстро, как простая итерация по идентификатору. Однако; он занимает довольно много оперативной памяти.

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 строк таблицы, чтобы исключить случайные

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) выборка всех строк перед WHERE условия применения.

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

products_scope.limit(sample_size).sample(limit)

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