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

127

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

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

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

Итак, что такое "Rails 3 Way", чтобы найти случайную запись?

  • 1
    дубликат stackoverflow.com/questions/2752231/…
  • 9
    ^^ кроме того, что я специально искал оптимальный путь для Rails 3, что является главной целью вопроса.
Показать ещё 1 комментарий
Теги:
activerecord
random
ruby-on-rails-3

13 ответов

198
Лучший ответ
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-style

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

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;
  • 11
    Ваш первый пример не будет работать в MySQL - синтаксис для MySQL - Thing.first (: order => "RAND ()") (опасность написания SQL вместо использования абстракций ActiveRecord)
  • 0
    @ DanSingerman, да, это RAND() или RANDOM() конкретной БД. Спасибо
Показать ещё 7 комментариев
27

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

Используя

по RAND()

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

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

становится

SELECT users. * FROM users ORDER BY RAND (id) LIMIT 1

и требуется от 8 до 12 секунд.

Журнал рельсов:

Пользовательская нагрузка (11030.8ms) SELECT users. * FROM users ORDER BY RAND() LIMIT 1

из mysql explain

+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| 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 = Использование временного; FileSort).

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

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

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

Журнал рельсов:

Пользовательская загрузка (25,2 мс) SELECT id FROM users Пользовательская нагрузка (0.2ms) SELECT users. * FROM users ГДЕ users. id= 106854 LIMIT 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 раз быстрее!

UPDATE:

как указано 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 мс.

  • 0
    добавление «.id» после «последнего» во второй пример поможет избежать ошибки «не удалось найти модель без идентификатора». Например, User.find (users.first (Random.rand (users.length)). Last.id)
  • 0
    Предупреждение! В MySQL RAND(id) НЕ будет давать вам различный случайный порядок каждого запроса. Используйте RAND() если хотите различный порядок каждого запроса.
Показать ещё 3 комментария
11

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

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

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

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

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

SELECT * FROM users ORDER BY RANDOM() LIMIT 5
11

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

https://github.com/spilliton/randumb

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

5

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

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

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

В таблице с более чем миллионом строк этот запрос обычно занимает менее 2 мс.

  • 0
    Еще одним преимуществом вашего решения является использование функции take которая выдает запрос LIMIT(1) но возвращает один элемент вместо массива. Поэтому нам не нужно вызывать в first
  • 0
    Мне кажется, что записи в начале таблицы имеют более высокую вероятность, так как они выбраны таким образом, что может оказаться не тем, чего вы хотите достичь.
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
  • 0
    Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
  • 0
    если нет пользователей, и вы хотите получить 2, то вы получите ошибки. имеет смысл.
Показать ещё 1 комментарий
4

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

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

Случай1: поиск одной случайной записи источник: trevor turk site
Добавьте это в модель 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 случайных записей, однако стоит упомянуть, что если база данных будет особенно большой (миллионы записей), это не будет идеальным, и производительность будет затруднена. Я буду исполнять до нескольких тысяч записей, которые были достаточными для меня.

2

Работает в Rails 5 и является агностиком DB:

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

@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)
  • 0
    Это всегда требует последующих записей, которые должны быть, по крайней мере, документированы (поскольку это может не соответствовать желанию пользователя).
1

Если используется Oracle

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

Выход

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
1

Вы можете использовать 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 выберет пять записей из этого - чрезвычайно расточительно.
  • 5
    sample не находится в ActiveRecord, образец находится в массиве. api.rubyonrails.org/classes/Array.html#method-i-sample
Показать ещё 2 комментария
-2

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

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

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

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню