Rails 3: Отримайте випадковий запис


132

Отже, я знайшов кілька прикладів пошуку випадкової записи в Rails 2 - кращим методом, здається, є:

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

Будучи чимось новачком, я не впевнений, як це можна побудувати, використовуючи новий синтаксис пошуку в Rails 3.

Отже, що таке "Рейки 3 шляху", щоб знайти випадковий запис?



9
^^ окрім того, що я спеціально шукаю оптимальний спосіб Rails 3, в чому полягає вся мета цього питання.
Андрій

рейки 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;

11
Ваш перший приклад не працюватиме в MySQL, але синтаксис для MySQL є Thing.first (: order => "RAND ()") (небезпека написання SQL, а не використання абстракцій ActiveRecord)
DanSingerman

@ DanSingerman, так це специфічно для БД RAND()або RANDOM(). Дякую
fl00r

І це не створить проблем, якщо в індексі відсутні пункти? (якщо щось посередині стека буде видалено, чи є шанс, що його запитають?
Victor S

@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 ), де db знаходиться в localhost, а таблиця користувачів має трохи більше 100K записів .

Використання

замовлення RAND ()

йде досить повільно

User.order ("RAND (id)")

стає

ВИБІР users. * ВІД usersЗАМОВЛЕННЯ ПО РАНДІ (id) ГРАНІ 1

і займає від 8 до 12 секунд щоб відповісти !!

Журнал рейок:

Навантаження користувача (11030,8 мс) ВИБІР users. * З usersЗАМОВЛЕННЯ ПО РАНДУ () ГРАНІ 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 |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

Ви можете бачити, що не використовується індекс ( можливі_keys = NULL ), створюється тимчасова таблиця і потрібен додатковий пропуск для отримання потрібного значення ( extra = Використання тимчасового; Використання файлуort ).

З іншого боку, розділивши запит на дві частини та використовуючи Ruby, ми маємо розумне покращення часу відповіді.

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

(; нуль для використання консолі)

Журнал рейок:

Навантаження користувача (25.2ms) ВИБРАТИ ідентифікатор з usersкористувальницької навантаження (0,2 мс) ВИБРАТИ users. * FROM usersWHERE 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 мс.


додавання ".id" після "останнього" у ваш другий приклад дозволить уникнути помилки "не вдалося знайти модель без ідентифікатора". Напр. 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

Також користувачі = User.scoped.select (: id); нуль не застаріло. Використовуйте це замість: users = User.where (nil) .select (: id)
icantbecool

Я вважаю, що в якості параметра спочатку використовується Random.rand (users.length) - помилка. Random.rand може повернути 0. Коли спочатку використовується параметр 0, обмеження встановлюється нулем, і це не повертає записів. Замість цього потрібно використовувати 1 + Випадкові (users.length), якщо користувачі мають довжину> 0.
SWoo

12

Якщо ви використовуєте Postgres

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

Якщо використовується MySQL

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

В обох випадках ви вибираєте 5 записів випадково з таблиці Користувачі. Ось власне запит SQL, відображений у консолі.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

11

Для цього я зробив 3 дорогоцінні камені для рейок, які краще працюють на великих столах і дозволяють ланцюжкові відносини та сфери застосування:

https://github.com/spilliton/randumb

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


6

Багато опублікованих відповідей насправді не працюватимуть на досить великих таблицях (1+ мільйонів рядків). Випадкове замовлення швидко займає декілька секунд, а підрахунок за столом також займає досить багато часу.

Рішення, яке добре працює для мене в цій ситуації, - це використовувати RANDOM()за умови, де:

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

У таблиці, що містить понад мільйон рядків, цей запит зазвичай займає менше 2 мс.


Ще однією перевагою вашого рішення є використання takeфункції, яка дає LIMIT(1)запит, але повертає один елемент замість масиву. Тож нам не потрібно посилатисяfirst
Пьотр Галас

Мені здається, що записи на початку таблиці мають більш високу ймовірність, обрану таким чином, що може бути не тим, чого ви хочете досягти.
горн

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
Другий підхід не буде працювати з постгресами, але ви можете використовувати "RANDOM()"натомість ...
Даніель Ріхтер

4

Це було дуже корисно для мене, проте мені було потрібно трохи більшої гнучкості, тому я це робив:

Case1: Пошук одного випадкового джерела запису : сайт
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

Case2: Пошук декількох випадкових записів (без повторів) Джерело: не можу пригадати,
мені потрібно було знайти 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 випадкових записів, проте варто згадати, що якщо база даних особливо велика (мільйони записів), це не буде ідеальним, а продуктивність буде утруднена. Зробить добре до кількох тисяч записів, що мені було достатньо.


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 }

Насправді, #countзробить дзвінок у БД для COUNT. Якщо запис уже завантажений, це може бути поганою ідеєю. #sizeНатомість використовуватиметься рефактор, оскільки він вирішить, чи #countслід його використовувати, або, якщо запис уже завантажений, використовувати #length.
BenMorganIO

Вибрано з countна sizeоснові ваших відгуків. Більше інформації за адресою: dev.mensfeld.pl/2014/09/…
Dan Kohn

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)

Це завжди потребує подальших записів, які потрібно принаймні задокументувати (оскільки це може бути не те, що хоче користувач).
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

5
sampleне в ActiveRecord, зразок знаходиться в масиві. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans

3
Це дорогий спосіб отримати випадковий запис, особливо з великої таблиці. Рейки завантажують об'єкт для кожного запису з вашої таблиці в пам'ять. Якщо вам потрібен доказ, запустіть "консоль рейки", спробуйте "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" на кількість потрібних випадкових записів.


1
ні, частина Model.pluck (: id) .sample (3) не є дешевою. Він прочитає поле id для кожного елемента таблиці.
Максиміліано Гузман

Чи існує більш швидкий аґностичний спосіб баз даних?
Arcolye

-5

Я щойно зіткнувся з цим питанням, розробляючи невелику програму, де хотів вибрати випадкове запитання зі своєї БД. Я використав:

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

І це добре працює для мене. Я не можу говорити про те, якою є ефективність для великих БД, оскільки це лише невелика програма.


Так, це просто отримання всіх ваших записів та використання методів масиву рубіну. Недолік тут, звичайно, означає, що він завантажує всі ваші записи в пам'ять, потім їх випадковим чином упорядковує, а потім захоплює другий елемент в упорядкованому масиві. Це, безумовно, може бути свиною пам'яті, якби ви мали справу з великим набором даних. Незначна частина, чому б не схопити перший елемент? (тобто shuffle[0])
Андрій

повинні бути перетасовані [0]
Marcelo Austria
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.