Rails 3: получить случайную запись

132

Итак, я нашел несколько примеров поиска случайной записи в Rails 2 - кажется, предпочтительный метод:

Thing.find :first, :offset => rand(Thing.count)

Будучи новичком, я не уверен, как это можно построить с использованием нового синтаксиса find в Rails 3.

Итак, что же такое «Rails 3 Way» для поиска случайной записи?

Андрей
источник
1
дубликат stackoverflow.com/questions/2752231/…
fl00r
9
^^ за исключением того, что я специально ищу оптимальный способ Rails 3, что и является всей целью вопроса.
Эндрю
rails 3 - это только цепочка запросов :)
fl00r

Ответы:

216
Thing.first(:order => "RANDOM()") # For MySQL :order => "RAND()", - thanx, @DanSingerman
# Rails 3
Thing.order("RANDOM()").first

или

Thing.first(:offset => rand(Thing.count))
# Rails 3
Thing.offset(rand(Thing.count)).first

Собственно, в Rails 3 все примеры будут работать. Но использование порядка RANDOMдовольно медленно для больших таблиц, но больше в стиле sql

UPD. Вы можете использовать следующий трюк с индексированным столбцом (синтаксис PostgreSQL):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;
fl00r
источник
11
Ваш первый пример не будет работать в MySQL - синтаксис для MySQL - Thing.first (: order => "RAND ()") (опасность написания SQL вместо использования абстракций ActiveRecord)
DanSingerman,
@ DanSingerman, да это конкретно БД RAND()или RANDOM(). Спасибо
fl00r
И это не создаст проблем, если в индексе отсутствуют элементы? (если что-то в середине стека будет удалено, будет ли шанс, что это будет запрошено?
Виктор С.
@VictorS, нет, #offset просто переходит к следующей доступной записи. Я тестировал его на Ruby 1.9.2 и Rails 3.1
SooDesuNe
1
@JohnMerlino, да, 0 - это смещение, а не идентификатор. Offet 0 означает первый элемент согласно заказу.
fl00r
29

Я работаю над проектом ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ), где база данных находится в локальном хосте, а таблица пользователей имеет немного более 100 КБ записей .

С помощью

заказать по RAND ()

довольно медленно

User.order ( "RAND (идентификатор)"). Первый

становится

ВЫБРАТЬ users. * ИЗ usersORDER BY RAND (id) LIMIT 1

и на ответ требуется от 8 до 12 секунд !!

Журнал Rails:

Пользовательская нагрузка (11030,8 мс) SELECT users. * FROM usersORDER BY RAND () LIMIT 1

из mysql объяснить

+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra                           |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 110165 | Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

Вы можете видеть, что индекс не используется ( possible_keys = NULL ), создается временная таблица и требуется дополнительный проход для извлечения желаемого значения ( extra = Использование временного; Использование файловой сортировки ).

С другой стороны, разделив запрос на две части и используя Ruby, мы получаем разумное улучшение времени отклика.

users = User.scoped.select(:id);nil
User.find( users.first( Random.rand( users.length )).last )

(; nil для использования в консоли)

Журнал Rails:

Пользовательская нагрузка (25,2 мс) ВЫБРАТЬ идентификатор ИЗ usersпользовательской нагрузки (0,2 мс) ВЫБРАТЬ users. * ОТ usersГДЕ users. id= 106854 ПРЕДЕЛ 1

и mysql объясняет, почему:

+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| id | select_type | table | type  | possible_keys | key                      | key_len | ref  | rows   | Extra       |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
|  1 | SIMPLE      | users | index | NULL          | index_users_on_user_type | 2       | NULL | 110165 | Using index |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+

+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | PRIMARY       | PRIMARY | 4       | const |    1 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

теперь мы можем использовать только индексы и первичный ключ и выполнять работу примерно в 500 раз быстрее!

ОБНОВИТЬ:

как указано icantbecool в комментариях, вышеупомянутое решение имеет недостаток, если в таблице есть удаленные записи.

Обходной путь в этом может быть

users_count = User.count
User.scoped.limit(1).offset(rand(users_count)).first

что означает два запроса

SELECT COUNT(*) FROM `users`
SELECT `users`.* FROM `users` LIMIT 1 OFFSET 148794

и работает примерно за 500 мс.

xlembouras
источник
добавление ".id" после "last" во второй пример позволит избежать ошибки "не удалось найти модель без идентификатора". Например, User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine
Предупреждение! В MySQL НЕRAND(id) будет задавать разный случайный порядок для каждого запроса. Используйте, RAND()если хотите, чтобы каждый запрос был в другом порядке.
Джастин Таннер
User.find (users.first (Random.rand (users.length)). Last.id) не будет работать, если была удалена запись. [1,2,4,5,] и потенциально может выбрать идентификатор 3, но не будет активного отношения записи.
icantbecool 02
Кроме того, users = User.scoped.select (: id); nil не является устаревшим. Используйте вместо этого: users = User.where (nil) .select (: id)
icantbecool
Я считаю, что использование Random.rand (users.length) в качестве первого параметра является ошибкой. Random.rand может возвращать 0. Когда 0 используется в качестве параметра для first, предел устанавливается на ноль, и это не возвращает никаких записей. Вместо этого следует использовать 1 + Random (users.length) при условии, что users.length> 0.
SWoo
12

Если вы используете Postgres

User.limit(5).order("RANDOM()")

При использовании MySQL

User.limit(5).order("RAND()")

В обоих случаях вы случайным образом выбираете 5 записей из таблицы Users. Вот фактический запрос SQL, отображаемый в консоли.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5
icantbecool
источник
11

Для этого я сделал жемчужину rails 3, которая лучше работает с большими таблицами и позволяет связывать отношения и области:

https://github.com/spilliton/randumb

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

spilliton
источник
6

Многие из опубликованных ответов на самом деле не будут работать с довольно большими таблицами (более 1 миллиона строк). Произвольный порядок быстро занимает несколько секунд, и подсчет по таблице также занимает довольно много времени.

Решение, которое хорошо работает для меня в этой ситуации, заключается в использовании RANDOM()условия where:

Thing.where('RANDOM() >= 0.9').take

Для таблицы, содержащей более миллиона строк, этот запрос обычно занимает менее 2 мс.

fivedigit
источник
Еще одним преимуществом вашего решения является использование takeфункции, которая выдает LIMIT(1)запрос, но возвращает один элемент вместо массива. Так что ссылаться не нужноfirst
Петр Галас
Мне кажется, что записи в начале таблицы имеют более высокую вероятность выбора таким образом, что может быть не тем, чего вы хотите достичь.
gorn
5

вот так

рельсы путь

#in your initializer
module ActiveRecord
  class Base
    def self.random
      if (c = count) != 0
        find(:first, :offset =>rand(c))
      end
    end
  end
end

использование

Model.random #returns single random object

или вторая мысль

module ActiveRecord
  class Base
    def self.random
      order("RAND()")
    end
  end
end

использование:

Model.random #returns shuffled collection
Тим Кречмер
источник
Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Бруно
если пользователей нет и вы хотите получить 2, вы получите ошибку. имеет смысл.
Тим Кречмер
1
Второй подход не будет работать с postgres, но вы можете использовать его "RANDOM()"вместо этого ...
Дэниел Рихтер
4

Это было очень полезно для меня, но мне нужно было немного больше гибкости, поэтому я сделал следующее:

Случай 1: поиск одного случайного источника записи : сайт trevor
turk Добавьте это в модель Thing.rb

def self.random
    ids = connection.select_all("SELECT id FROM things")
    find(ids[rand(ids.length)]["id"].to_i) unless ids.blank?
end

тогда в вашем контроллере вы можете вызвать что-то вроде этого

@thing = Thing.random

Случай 2: поиск нескольких случайных записей (без повторов). Источник: не могу вспомнить,
мне нужно было найти 10 случайных записей без повторов, так что это то, что я нашел, работало
В вашем контроллере:

thing_ids = Thing.find( :all, :select => 'id' ).map( &:id )
@things = Thing.find( (1..10).map { thing_ids.delete_at( thing_ids.size * rand ) } )

Будет найдено 10 случайных записей, однако стоит упомянуть, что если база данных особенно велика (миллионы записей), это будет не идеально, и производительность будет снижена. Is будет работать до нескольких тысяч записей, чего мне было достаточно.

Hishalv
источник
4

Метод рубина для случайного выбора элемента из списка 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 }
Дэн Кон
источник
Собственно, #countвызовет БД для файла COUNT. Если запись уже загружена, это может быть плохой идеей. #sizeВместо этого можно было бы использовать рефакторинг, поскольку он решит, #countследует ли его использовать или, если запись уже загружена, использовать #length.
BenMorganIO
Переключено с countна sizeна основе вашего отзыва. Дополнительная информация: dev.mensfeld.pl/2014/09/…
Дэн Кон
3

Работает в Rails 5 и не зависит от БД:

Это в вашем контроллере:

@quotes = Quote.offset(rand(Quote.count - 3)).limit(3)

Вы, конечно, можете указать на это, как показано здесь .

приложение / модель / проблемы / randomable.rb

module Randomable
  extend ActiveSupport::Concern

  class_methods do
    def random(the_count = 1)
      records = offset(rand(count - the_count)).limit(the_count)
      the_count == 1 ? records.first : records
    end
  end
end

затем...

приложение / модели / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Тогда вы можете просто использовать:

Books.random

или

Books.random(3)
richardun
источник
При этом всегда требуются последующие записи, которые необходимо как минимум задокументировать (поскольку это может быть не то, что хочет пользователь).
gorn
2

Вы можете использовать sample () в ActiveRecord

Например

def get_random_things_for_home_page
  find(:all).sample(5)
end

Источник: http://thinkingeek.com/2011/07/04/easily-select-random-records-rails/

Тронд
источник
33
Это очень плохой запрос для использования, если у вас есть большое количество записей, так как БД выберет ВСЕ записи, а затем Rails выберет из них пять записей - очень расточительно.
DaveStephens 02
5
sampleотсутствует в ActiveRecord, образец находится в массиве. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans
3
Это дорогой способ получить случайную запись, особенно из большой таблицы. Rails загрузит объект для каждой записи из вашей таблицы в память. Если вам нужны доказательства, запустите консоль rails, попробуйте SomeModelFromYourApp.find (: all) .sample (5) и посмотрите полученный SQL.
Элиот Сайкс
1
Посмотрите мой ответ, который превращает этот дорогостоящий ответ в обтекаемую красоту для получения нескольких случайных записей.
Arcolye
1

При использовании Oracle

User.limit(10).order("DBMS_RANDOM.VALUE")

Вывод

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
Марсело Австрия
источник
1

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

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 пользователей. Чем больше у вас записей, тем значительнее будет разница в производительности.


ОБНОВИТЬ:

Выполнить на таблице с 550 000 строками

  1. Model.where(id: Model.pluck(:id).sample(10)) Стоимость 1384.0ms

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

  1. gem: quick_random_recordsтолько стоимость 6.4msполностью

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

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

Очень простой способ получить несколько случайных записей из таблицы. Это делает 2 дешевых запроса.

Model.where(id: Model.pluck(:id).sample(3))

Вы можете изменить цифру «3» на нужное количество случайных записей.

Arcolye
источник
1
нет, часть Model.pluck (: id) .sample (3) стоит недешево. Он будет читать поле id для каждого элемента в таблице.
Максимилиано Гусман
Есть ли более быстрый способ, не зависящий от базы данных?
Arcolye
-5

Я только что столкнулся с этой проблемой при разработке небольшого приложения, в котором я хотел выбрать случайный вопрос из моей БД. Я использовал:

@question1 = Question.where(:lesson_id => params[:lesson_id]).shuffle[1]

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

rails_newbie
источник
Да, это просто получение всех ваших записей и использование для них методов рубинового массива. Недостаток, конечно, заключается в том, что это означает загрузку всех ваших записей в память, затем случайным образом переупорядочить их, а затем захватить второй элемент в переупорядоченном массиве. Это определенно может привести к потере памяти, если вы имеете дело с большим набором данных. Помимо второстепенных, почему бы не взять первый элемент? (т.е. shuffle[0])
Эндрю
необходимо перемешать [0]
Марсело Австрия