Випадковий запис в ActiveRecord


151

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

Однак я також натрапив на інший спосіб пошуку в Google (не можу приписувати посилання через нові обмеження користувача):

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

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


2
2 бали, які можуть допомогти відповісти. 1. Наскільки рівномірно розподілені ваші ідентифікатори, вони послідовні? 2. Якою вона повинна бути випадковою? Досить хороший випадковий, чи справжній випадковий?
Майкл

Вони є послідовними ідентифікаторами, які автоматично генеруються activerecord, і він повинен бути достатньо хорошим.
jyunderwood

1
Тоді пропоноване вам рішення наближене до ідеального :) Я б використав "SELECT MAX (id) FROM table_name" замість COUNT (*), оскільки він буде краще обробляти видалені рядки, інакше все інше добре. Коротше кажучи, якщо "досить добре" все в порядку, то вам просто потрібно мати метод, який передбачає розподіл, близький до того, що ви є насправді. Якщо він рівномірний і, як ви вже говорили, простий ранд чудово працює.
Майкл

1
Це не спрацює, якщо ви видалили рядки.
Венкат Д.

Відповіді:


136

Я не знайшов ідеального способу зробити це без принаймні двох запитів.

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

offset = rand(Model.count)

# Rails 4
rand_record = Model.offset(offset).first

# Rails 3
rand_record = Model.first(:offset => offset)

Якщо чесно, я щойно використовував ЗАМОВЛЕННЯ RAND () або RANDOM () (залежно від бази даних). Це не проблема ефективності, якщо у вас немає проблеми з продуктивністю.


2
Код Model.find(:offset => offset).firstпризведе до помилки. Я думаю, що Model.first(:offset => offset)може бути краще.
Харіш Шетті

1
так, я працюю з Rails 3 і постійно плутаюсь щодо форматів запитів між версіями.
Toby Hede

7
Зауважте, що використання зміщення дуже повільне з великим набором даних, оскільки воно фактично потребує сканування індексу (або сканування таблиці), якщо кластерний індекс використовується як InnoDB). Іншими словами, це операція O (N), але "WHERE id> = # {rand_id} ORDER BY id ASC LIMIT 1" - це O (log N), що набагато швидше.
kenn

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

Caveat : для великих наборів даних з мільйонами записів це повільно, тому ви можете додати limitпункт.


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

7
ORDER BY RAND () для MySQL закінчується жахливим часом виконання, оскільки дані збільшуються. Це неможливо (залежно від вимог часу), навіть починаючи з тисячі рядів.
Майкл

Майкл підкреслює чудовий момент (це стосується і інших БД). Як правило, вибір випадкових рядків з великих таблиць - це не те, що потрібно робити в динамічній дії. Кечінг - твій друг. Переосмислення того, що ви намагаєтеся зробити, теж не може бути поганою ідеєю.
семантичнийкарт

1
Замовити RAND () в mysql на столі з мільйонам рядків - sloooooooooooooooooooooww.
Subimage

24
Більше не працює. Використовуйте Model.order("RANDOM()").firstзамість цього.
phil pirozhkov

Повільний і специфічний для бази даних. 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 виявляється набагато повільніше.

EDIT Я також спробував

Product.first(:order => "RAND()")

Але мені довелося вбити його через ~ 60 секунд. MySQL був "Копіювання в tmp таблицю на диску". Це не вийде.


1
Для тих, хто шукає додаткових тестів, скільки часу займає реальний випадковий підхід: я спробував Thing.order("RANDOM()").firstтаблицю з 250 000 записів - запит закінчився за півсекунди. (PostgreSQL 9.0, REE 1.8.7, ядра 2 x 2.66 ГГц) Це досить швидко для мене, оскільки я роблю разову "очистку".
Флоріан Пільц

6
Ранді метод 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
Це чисто і працює для невеликого столу або одноразового використання, лише зауважте, що він не буде масштабуватися. У таблиці 3М викрадення ідентифікаторів займає приблизно 15 секунд для мене на MariaDB.
mahemoff

2
Це хороший момент. Ви знайшли швидше альтернативне рішення, зберігаючи ті самі якості?
Нільс Б.

Чи не прийняте рішення про зміщення не підтримує однакових якостей?
mahemoff

Ні, він не підтримує умов і не має однакової ймовірності вибору для таблиць із видаленими записами.
Нільс Б.

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

15

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

Model.all.sample

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


99
Не роби цього. Колись.
Забба

5
Якщо у вашій базі даних є 100k рядків, все це повинно бути завантажене в пам'ять.
Венкат Д.

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

13
Будь ласка - ніколи не кажіть ніколи. Це відмінне рішення для налагодження в часі розвитку, якщо таблиця невелика. (І якщо ви берете зразки, налагодження цілком можливо у випадку використання).
mahemoff

Я використовую для висіву насіння і добре для мене. Крім того, Model.all.sample (n) теж працює :)
Arnaldo Ignacio Gaspar Véjar

13

Я зробив 3 рейки для цього:

https://github.com/spilliton/randumb

Це дозволяє робити такі речі:

Model.where(:column => "value").random(10)

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щоб повернути випадковий запис.


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, кількість рандів нараховується до кількості - 1
anemaria20

Спасибі, змінено: +1:
Томас Клемм

5

Читання всього цього не дало мені великої впевненості в тому, що з них найкраще працюватиме в моїй конкретній ситуації з Rails 5 та MySQL / Maria 5.5. Тож я перевірив деякі відповіді на ~ 65000 записів і маю два відмови:

  1. RAND () з a 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">

Я б не рекомендував працювати з методами масиву під час використання AR. Цей спосіб займає майже 8 разів час, коли order('rand()').limit(1)виконується "та сама" робота (із записом ~ 10 К).
Себастьян Пальма

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

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

Це можна зробити і за допомогою postgres з order('random()'MySQL order('rand()'). Це, безумовно, найкраща відповідь.
jrochkind

1

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


Це не працює на 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, написаної Гульчіном Йілдірімом .


1

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

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

Pluck виграє 100 разів на моїй 25 000 000 таблиці рядків Редагувати: насправді цей час включає планку в циклі, якщо я вийму його, він працює приблизно так само швидко, як і проста ітерація на 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)

Ось дані, що працюють у моїй 100 000 таблиці рядків 2000 разів, щоб виключити випадкові випадки

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

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)

Для мене набагато зрозуміло


0

Я пробую це на прикладі Сема на своєму додатку, використовуючи рейли 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)

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: Результати Benchmark у відповідях вище не є надійними (принаймні, у Postgres), тому що деякі запити БД, які виконуються у 2-й час, можуть бути значно швидшими, ніж виконання у 1-й раз, завдяки кешу DB. І, на жаль, не існує простого способу відключити кеш в 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

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