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

151

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

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

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

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

jyunderwood
источник
2
2 балла, которые могут помочь в ответе. 1. Насколько равномерно распределены ваши идентификаторы, они последовательные? 2. Насколько случайным оно должно быть? Достаточно хороший случайный или настоящий случайный?
Майкл
Это последовательные идентификаторы, которые автоматически генерируются activerecord, и это должно быть достаточно хорошо.
Jyunderwood
1
Тогда ваше предлагаемое решение близко к идеальному :) Я бы использовал «SELECT MAX (id) FROM table_name» вместо COUNT (*), так как он будет работать с удаленными строками немного лучше, иначе все остальное в порядке. Короче говоря, если «достаточно хорошо» в порядке, то вам просто нужен метод, который предполагает распределение, близкое к тому, что у вас есть на самом деле. Если он равномерный и, как вы уже сказали, простой rand отлично работает.
Майкл
1
Это не будет работать, когда вы удалили строки.
Венкат Д.

Ответы:

136

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

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

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
Код Model.find(:offset => offset).firstвыдаст ошибку. Я думаю, Model.first(:offset => offset)может работать лучше.
Хариш Шетти
1
да, я работал с Rails 3 и постоянно путаюсь с форматами запросов между версиями.
Тоби Хеде
7
Обратите внимание, что использование смещения очень медленно с большим набором данных, так как оно действительно требует сканирования индекса (или сканирования таблицы, в случае, если кластерный индекс используется как InnoDB). Другими словами, это операция O (N), но «WHERE id> = # {rand_id} ORDER BY id ASC LIMIT 1» - это O (log N), что намного быстрее.
Кенн
15
Имейте в виду, что подход со смещением дает только одну случайно найденную точку данных (первая, все после все еще сортируются по идентификатору). Если вам нужно несколько случайно выбранных записей, вы должны использовать этот подход несколько раз или использовать метод случайного порядка, предоставленный вашей базой данных, то есть Thing.order("RANDOM()").limit(100)для 100 случайно выбранных записей. (Имейте в виду, что это RANDOM()в PostgreSQL и RAND()в MySQL ... не так переносимо, как хотелось бы.)
Флориан Пилз
3
У меня не работает на Rails 4. Используйте Model.offset(offset).first.
mahemoff
206

Рельсы 6

Как заявил Джейсон в комментариях, в Rails 6 неатрибутные аргументы не допускаются. Вы должны заключить значение в Arel.sql()утверждение.

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

Рельсы 5, 4

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

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

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

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

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

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

Мохамад
источник
4
«Random ()» также работает в sqlite, поэтому для тех из нас, кто все еще разрабатывает sqlite и работает postgres в производстве, ваше решение работает в обеих средах.
wuliwong
5
Я создал эталон для этого против принятого ответа. На Postgresql 9.4 подход этого ответа примерно в два раза быстрее.
Панмари
3
Похоже, это не рекомендуется на mysql webtrenches.com/post.cfm/avoid-rand-in-mysql
Пракаш Мурти
Это самое быстрое решение
Серхио Белевский
1
«Аргументы без атрибутов будут запрещены в Rails 6.0. Этот метод не следует вызывать с предоставленными пользователем значениями, такими как параметры запроса или атрибуты модели. Известные безопасные значения могут быть переданы путем переноса их в Arel.sql ().»
Трентон Тайлер
73

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

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

Model.first(:order => "RANDOM()") # postgres example
semanticart
источник
7
ORDER BY RAND () для MySQL заканчивается ужасной средой исполнения при увеличении объема данных. Это не поддерживается (в зависимости от требований времени), даже начиная с тысяч строк.
Майкл
Майкл поднимает замечательный вопрос (это верно и для других БД). Обычно выбор случайных строк из больших таблиц - это не то, что вы хотите делать в динамическом действии. Кэширование - твой друг. Переосмысление того, что вы пытаетесь достичь, тоже не может быть плохой идеей.
semanticart
1
Порядок RAND () в mysql для таблицы с примерно миллионом строк - это неаккуратно.
Subimage
24
Больше не работает Используйте Model.order("RANDOM()").firstвместо этого.
Фил Пирожков
Медленный и специфичный для базы данных. ActiveRecord должен беспрепятственно работать между базами данных, поэтому вам не следует использовать этот метод.
Dex
29

Сравнительный анализ этих двух методов в 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 на диске". Это не сработает.

dkam
источник
1
Для тех, кто ищет дополнительные тесты, сколько времени занимает реальный случайный подход: я попробовал Thing.order("RANDOM()").firstтаблицу с 250 тыс. Записей - запрос завершился менее чем за полсекунды. (PostgreSQL 9.0, REE 1.8.7, 2 x 2,66 ГГц ядра) Это достаточно быстро для меня, так как я делаю одноразовую «очистку».
Флориан Пилз
6
Метод rand в Ruby возвращает на единицу меньше указанного числа, поэтому вы захотите rand_id = rand(Product.count) + 1или никогда не получите последнюю запись.
Ричи
4
Примечание random1не будет работать, если вы удалите строку в таблице. (Количество будет меньше максимального идентификатора, и вы никогда не сможете выбрать строки с высоким идентификатором).
Николас
Использование random2может быть улучшено #orderиспользованием индексированного столбца.
Карсон Рейнке
18

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

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

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

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

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

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

Нильс Б.
источник
8
Это чисто и работает для небольшого стола или единовременного использования, только заметьте, что оно не масштабируется. На столе 3M вытащение идентификаторов занимает около 15 секунд для меня на MariaDB.
mahemoff
2
Неплохо подмечено. Вы нашли альтернативное решение, которое быстрее, сохраняя те же качества?
Нильс Б.
Разве принятое офсетное решение не поддерживает те же качества?
mahemoff
Нет, он не поддерживает условия и не имеет равной вероятности выбора для таблиц с удаленными записями.
Нильс Б.
1
Если подумать, если вы применяете ограничения при подсчете и выборе со смещением, техника должна работать. Я представлял себе только применение его на счет.
Нильс Б.
15

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

Model.all.sample

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

Райан Аталла
источник
99
Не делай этого. Когда-либо.
Забба
5
Если у вас есть 100 тыс. Строк в вашей базе данных, все они должны быть загружены в память.
Венкат Д.
3
Конечно, это не рекомендуется для производственного кода реального времени, но мне нравится это решение, его очень легко использовать для особых ситуаций, таких как заполнение базы данных поддельными значениями.
fguillen
13
Пожалуйста - никогда не говори никогда. Это отличное решение для отладки во время разработки, если таблица мала. (И если вы берете образцы, отладка вполне возможна).
mahemoff
Я использую для посева и это хорошо для меня. Кроме того, Model.all.sample (n) тоже работает :)
Арнальдо Игнасио Гаспар Вехар
13

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

https://github.com/spilliton/randumb

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

Model.where(:column => "value").random(10)
spilliton
источник
7
В документации этого драгоценного камня они объясняют, что «randumb просто добавляет дополнительный ORDER BY RANDOM()(или RAND()для mysql) к вашему запросу». - следовательно, комментарии о плохой производительности, упомянутые в комментариях к ответу @semanticart, также применимы при использовании этого драгоценного камня. Но, по крайней мере, это не зависит от БД.
Николя
8

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

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

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

Knotty66
источник
1
тебе нужно limit(1)? ActiveRecord#firstдолжен быть достаточно умен, чтобы сделать это.
Tokland
6

Один запрос в 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)
Томас Клемм
источник
1
Нет нужды в -1, ранд считает до num - 1
anemaria20
Спасибо, изменилось: +1:
Томас Клемм
5

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

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

Сэм
источник
3

Вы можете использовать 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">
trejo08
источник
Я бы не рекомендовал работать с методами массива при использовании AR. Этот способ занимает почти 8 раз времени, order('rand()').limit(1)делает «ту же самую» работу (с ~ 10K записями).
Себастьян Пальма
3

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

https://github.com/haopingfan/quick_random_records

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

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

введите описание изображения здесь

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

введите описание изображения здесь

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

введите описание изображения здесь

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

введите описание изображения здесь


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

Дерек Фан
источник
2

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

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

rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)
Юрий Карпович
источник
1

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

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

Дэн Кон
источник
1

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
mahatmanich
источник
Вы можете сделать это с помощью postgres order('random()'и MySQL order('rand()'). Это определенно лучший ответ.
jrochkind
1

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

Вадим Еремеев
источник
Это не работает на MySQL. Вы должны включить, по крайней мере, с каким механизмом БД это должно работать
Арнольд Роа
Извините, была опечатка. Исправлено сейчас. Должен работать на MySQL (только)
Вадим Еремеев
1

Если вы используете 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, написанного Gulcin Yildirim .

Адам Шихан
источник
1

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

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

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

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

rand_record = Model.all.shuffle

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

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

rand_record = Model.all.shuffle.first
Gregdebrick
источник
1
Не лучший вариант, так как при этом все записи загружаются в память. Также shuffle.first==.sample
Андрей Роженко
0

Я новичок в RoR, но у меня это получилось:

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

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

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

Аарон Пеннингтон
источник
4
Плохо то, что он загружает все карты из базы данных. Более эффективно делать это внутри базы данных.
Антон Кузьмин
Вы также можете перетасовать массивы с помощью array.shuffle. В любом случае, будьте осторожны, так как Card.allвсе записи карточки будут загружены в память, которая становится все более неэффективной, чем больше объектов, о которых мы говорим.
Томас Клемм
0

Что делать?

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

Для меня это очень понятно

poramo
источник
0

Я пытаюсь использовать этот пример Сэма в моем приложении, используя 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)
RLD
источник
0

.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 нет простого способа отключить кэш, чтобы сделать эти тесты надежными.

Линь Дам
источник
0

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

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

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

Дэмиен Рош
источник
0

В зависимости от значения «случайный» и того, что вы на самом деле хотите сделать, take может быть достаточно.

Под «значением» случайного я подразумеваю:

  • Ты имеешь в виду, дай мне какой-нибудь элемент, мне все равно, его позиция? тогда этого достаточно.
  • Теперь, если вы имеете в виду «дайте мне какой-либо элемент с достаточной вероятностью того, что повторные эксперименты дадут мне другие элементы из набора», тогда форсируйте «Удачу» любым из методов, упомянутых в других ответах.

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

https://guides.rubyonrails.org/active_record_querying.html#take

jgomo3
источник