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

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

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

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

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

Ответ 1

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

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

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

В Rails 4 и 5, используя Postgresql или SQLite, используя RANDOM():

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

https://github.com/spilliton/randumb

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

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

Ответ 7

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

Model.all.sample

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

Чтение всего этого не давало мне уверенности в том, что из них лучше всего подойдет в моей конкретной ситуации с 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)

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

Ответ 11

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

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

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

Ответ 12

Вы можете использовать Array метод sample, метод sample возвращает случайный объект из массива, для того, чтобы использовать его нужно просто EXEC в простом ActiveRecord запроса, который возвращает коллекцию, например:

User.all.sample

вернет что-то вроде этого:

#<User id: 25, name: "John Doe", email: "[email protected]", created_at: "2018-04-16 19:31:12", updated_at: "2018-04-16 19:31:12">

Ответ 13

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

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

Ответ 14

Для базы данных MySQL попробуйте: Model.order( "RAND()" ). first

Ответ 15

Rails 4.2 и 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

Ответ 16

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

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

-- 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 полностью случайным или если он просто вернет первую строку из случайной страницы.

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

Ответ 17

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

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

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

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)

Ответ 18

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

https://github.com/haopingfan/quick_random_records

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

  1. quick_random_records стоит всего 4.6ms.

enter image description here

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

enter image description here

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

enter image description here

  1. User.all.sample(10) стоит 573.4ms.

enter image description here


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

Ответ 20

Что делать:

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

Для меня очень ясно

Ответ 21

Я пытаюсь использовать этот пример Сэма в своем приложении, используя rails 4.2.8 из Benchmark (я помещаю 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)

Ответ 22

.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), потому что некоторые запросы к БД, выполняющиеся во 2-й раз, могут быть значительно быстрее, чем в 1-й раз, благодаря кешу БД. И, к сожалению, в Postgres нет простого способа отключить кэш, чтобы сделать эти тесты надежными.

Ответ 23

Очень старый вопрос, но с:

rand_record = Model.all.shuffle

Вы получили массив записей, отсортированный по случайному порядку. Не нужно драгоценных камней или скриптов.

Если вы хотите одну запись:

rand_record = Model.all.shuffle.first

Ответ 24

Наряду с использованием RANDOM(), вы также можете добавить это в область видимости:

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

Или, если вам это не нравится, просто добавьте его в метод класса. Теперь Thing.random работает вместе с Thing.random(n).