Случайная запись в 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 записей и получил два:
- RAND() с
limit
- явный победитель. - Не используйте
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
Все остальные ответы плохо работают с большой базой данных, кроме этого гема:
- quick_random_records стоит всего
4.6ms
.
![enter image description here]()
-
User.order('RAND()').limit(10)
стоит 733.0ms
.
![enter image description here]()
- принятый подход
offset
ответа стоил всего 245.4ms
.
![enter image description here]()
-
User.all.sample(10)
стоит 573.4ms
.
![enter image description here]()
Примечание. В моей таблице всего 120 000 пользователей. Чем больше у вас записей, тем больше будет разница в производительности.
Ответ 19
Я новичок в RoR, но у меня это получилось для меня:
def random
@cards = Card.all.sort_by { rand }
end
Это произошло из:
Как произвольно сортировать (скремблировать) массив в Ruby?
Ответ 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)
.