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