Rails 3: Get Random Record

132

Znalazłem więc kilka przykładów wyszukiwania losowego rekordu w Railsach 2 - preferowaną metodą wydaje się być:

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

Będąc nowicjuszem, nie jestem pewien, jak można to skonstruować przy użyciu nowej składni wyszukiwania w Railsach 3.

Więc co to jest "Rails 3 Way", aby znaleźć losowy rekord?

Andrzej
źródło
9
^^ z wyjątkiem tego, że szczególnie szukam optymalnego sposobu Rails 3, co jest celem całego pytania.
Andrew
specyficzny dla rails 3 jest tylko łańcuch zapytań :)
fl00r

Odpowiedzi:

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

lub

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

Właściwie w Railsach 3 wszystkie przykłady będą działać. Ale używanie kolejności RANDOMjest dość powolne w przypadku dużych tabel, ale bardziej w stylu sql

UPD. Możesz użyć następującej sztuczki na indeksowanej kolumnie (składnia PostgreSQL):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;
fl00r
źródło
11
Twój pierwszy przykład nie zadziała jednak w MySQL - składnia MySQL to Thing.first (: order => "RAND ()") (niebezpieczeństwo pisania SQL zamiast używania abstrakcji ActiveRecord)
DanSingerman
@ DanSingerman, tak, to jest specyficzne dla DB RAND()lub RANDOM(). Dzięki
fl00r
I to nie spowoduje problemów, jeśli w indeksie brakuje elementów? (jeśli coś na środku stosu zostanie usunięte, czy będzie szansa, że ​​zostanie o to poproszony?
Victor S,
@VictorS, nie, to nie #offset przejdzie do następnego dostępnego rekordu. Przetestowałem to z Ruby 1.9.2 i Rails 3.1
SooDesuNe
1
@JohnMerlino, tak 0 to przesunięcie, a nie identyfikator. Przesunięcie 0 oznacza pierwszą pozycję zgodnie z zamówieniem.
fl00r
29

Pracuję nad projektem ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ), w którym baza danych znajduje się w localhost, a tablica użytkowników ma nieco ponad 100K rekordów .

Za pomocą

order by RAND ()

jest dość powolny

User.order („RAND (id)”). First

staje się

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

a odpowiedź zajmuje od 8 do 12 sekund !!

Dziennik Railsów:

User Load (11030,8ms) SELECT users. * FROM usersORDER BY RAND () LIMIT 1

z wyjaśnienia 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 |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

Widać, że żaden indeks nie jest używany ( possible_keys = NULL ), tworzona jest tymczasowa tabela i wymagane jest dodatkowe przejście, aby pobrać żądaną wartość ( extra = Usingporary; Using filesort ).

Z drugiej strony, dzieląc zapytanie na dwie części i używając Rubiego, mamy znaczną poprawę czasu odpowiedzi.

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

(; zero w przypadku użycia konsoli)

Dziennik Railsów:

User Load (25,2 ms) SELECT id FROM User Load (0,2 ms users) SELECT users. * FROM usersWHERE users. id= 106854 LIMIT 1

a wyjaśnienie mysql udowadnia, dlaczego:

+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| 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 |       |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+

możemy teraz używać tylko indeksów i klucza podstawowego i wykonywać to zadanie około 500 razy szybciej!

AKTUALIZACJA:

jak zauważył icantbecool w komentarzach powyższe rozwiązanie ma wadę, jeśli w tabeli są usunięte rekordy.

Może to być obejście

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

co przekłada się na dwa zapytania

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

i działa w około 500 ms.

xlembouras
źródło
dodanie „.id” po „last” do drugiego przykładu pozwoli uniknąć błędu „nie można znaleźć modelu bez identyfikatora”. Np. User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine
Ostrzeżenie! W MySQL RAND(id)będzie NIE daje innym losowo za każdym zapytaniu. Użyj, RAND()jeśli chcesz, aby każde zapytanie miało inną kolejność.
Justin Tanner,
User.find (users.first (Random.rand (users.length)). Last.id) nie będzie działać, jeśli został usunięty rekord. [1,2,4,5,] i potencjalnie mógłby wybrać identyfikator 3, ale nie byłoby aktywnego rekordu relacji.
icantbecool
Ponadto users = User.scoped.select (: id); nil nie jest przestarzałe. Zamiast tego użyj tego: users = User.where (nil) .select (: id)
icantbecool
Uważam, że użycie Random.rand (users.length) jako parametru pierwszego jest błędem. Random.rand może zwrócić 0. Gdy jako pierwszy parametr jest używane 0, limit jest ustawiany na zero, a to nie zwraca żadnych rekordów. Zamiast tego należy użyć 1 + Random (users.length) przy założeniu users.length> 0.
SWoo
12

Jeśli używasz Postgres

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

Jeśli używasz MySQL

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

W obu przypadkach wybierasz losowo 5 rekordów z tabeli Users. Oto rzeczywiste zapytanie SQL wyświetlane w konsoli.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5
icantbecool
źródło
11

Zrobiłem za to klejnot rails 3, który działa lepiej na dużych stołach i pozwala łączyć relacje i zakresy:

https://github.com/spilliton/randumb

(edytuj): Domyślne zachowanie mojego klejnotu w zasadzie używa tego samego podejścia, co teraz, ale możesz użyć starego sposobu, jeśli chcesz :)

spilliton
źródło
6

Wiele z opublikowanych odpowiedzi w rzeczywistości nie będzie dobrze działać na dość dużych tabelach (ponad milion wierszy). Losowe zamówienie zajmuje szybko kilka sekund, a liczenie na stole również zajmuje dość dużo czasu.

Rozwiązaniem, które dobrze się sprawdza w tej sytuacji, jest użycie RANDOM()warunku gdzie:

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

W przypadku tabeli zawierającej ponad milion wierszy to zapytanie zajmuje zwykle mniej niż 2 ms.

fivedigit
źródło
Kolejną zaletą twojego rozwiązania jest użycie takefunkcji, która daje LIMIT(1)zapytanie, ale zwraca pojedynczy element zamiast tablicy. Więc nie musimy wzywaćfirst
Piotr Galas,
Wydaje mi się, że rekordy na początku tabeli mają większe prawdopodobieństwo, że zostaną wybrane w ten sposób, co może nie być tym, co chcesz osiągnąć.
gorn
5

No to ruszamy

szyny sposób

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

stosowanie

Model.random #returns single random object

albo druga myśl jest

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

stosowanie:

Model.random #returns shuffled collection
Tim Kretschmer
źródło
Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno
jeśli nie ma żadnych użytkowników, a chcesz uzyskać 2, otrzymujesz błędy. ma sens.
Tim Kretschmer
1
Drugie podejście nie zadziała z postgresem, ale możesz "RANDOM()"zamiast tego użyć ...
Daniel Richter
4

Było to dla mnie bardzo przydatne, ale potrzebowałem nieco większej elastyczności, więc zrobiłem to:

Przypadek 1: Znalezienie jednego losowego źródła rekordu : witryna trevor turk
Dodaj to do modelu 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

wtedy w kontrolerze możesz wywołać coś takiego

@thing = Thing.random

Przypadek 2: Znalezienie wielu losowych rekordów (bez powtórzeń) Źródło: nie pamiętam
, potrzebowałem znaleźć 10 losowych rekordów bez powtórzeń, więc znalazłem, że zadziałało w
twoim kontrolerze:

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

Znajdzie to 10 losowych rekordów, jednak warto wspomnieć, że jeśli baza danych jest szczególnie duża (miliony rekordów), nie byłoby to idealne, a wydajność będzie utrudniona. Zagra dobrze do kilku tysięcy płyt, co mi wystarczyło.

Hishalv
źródło
4

Metoda Ruby do losowego wybierania pozycji z listy to sample. Chcąc stworzyć wydajny sampledla ActiveRecord i na podstawie wcześniejszych odpowiedzi użyłem:

module ActiveRecord
  class Base
    def self.sample
      offset(rand(size)).first
    end
  end
end

Umieszczam to, lib/ext/sample.rba następnie ładuję to w config/initializers/monkey_patches.rb:

Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }
Dan Kohn
źródło
Właściwie #countwykona wywołanie DB dla pliku COUNT. Jeśli rekord jest już załadowany, może to być zły pomysł. #sizeZamiast tego należy użyć refaktora, ponieważ zdecyduje, czy #countnależy go użyć, czy, jeśli rekord jest już załadowany, użyć #length.
BenMorganIO,
Przełączono z countna sizena podstawie Twoich opinii. Więcej informacji na: dev.mensfeld.pl/2014/09/…
Dan Kohn
3

Działa w Rails 5 i jest agnostykiem DB:

To w twoim kontrolerze:

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

Możesz oczywiście umieścić to w obawie, jak pokazano tutaj .

app / models / problems / 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

następnie...

app / models / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Następnie możesz użyć po prostu, wykonując:

Books.random

lub

Books.random(3)
richardun
źródło
To zawsze wymaga kolejnych rekordów, które muszą być przynajmniej udokumentowane (ponieważ może nie być to, czego chce użytkownik).
gorn
2

Możesz użyć sample () w ActiveRecord

Na przykład

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

Źródło: http://thinkingeek.com/2011/07/04/easily-select-random-records-rails/

Trond
źródło
33
Jest to bardzo złe zapytanie, jeśli masz dużą liczbę rekordów, ponieważ DB wybierze WSZYSTKIE rekordy, a Railsy wybiorą z tego pięć rekordów - jest to bardzo marnotrawne.
DaveStephens
5
samplenie ma w ActiveRecord, sample znajduje się w Array. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans
3
Jest to drogi sposób na uzyskanie losowego rekordu, zwłaszcza z dużego stołu. Railsy załadują do pamięci obiekt dla każdego rekordu z Twojej tabeli. Jeśli potrzebujesz dowodu, uruchom „konsolę rails”, spróbuj „SomeModelFromYourApp.find (: all) .sample (5)” i spójrz na wygenerowany kod SQL.
Eliot Sykes
1
Zobacz moją odpowiedź, która zmienia tę kosztowną odpowiedź w usprawnione piękno umożliwiające uzyskanie wielu losowych rekordów.
Arcolye
1

Jeśli używasz Oracle

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

Wynik

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
Marcelo Austria
źródło
1

Zdecydowanie polecam ten klejnot dla losowych rekordów, który jest specjalnie zaprojektowany dla tabel z dużą ilością wierszy danych:

https://github.com/haopingfan/quick_random_records

Wszystkie inne odpowiedzi działają źle z dużą bazą danych, z wyjątkiem tego klejnotu:

  1. quick_random_records kosztuje tylko 4.6mscałkowicie.

wprowadź opis obrazu tutaj

  1. User.order('RAND()').limit(10)koszt zaakceptowanej odpowiedzi 733.0ms.

wprowadź opis obrazu tutaj

  1. offsetpodejście kosztować 245.4mscałkowicie.

wprowadź opis obrazu tutaj

  1. User.all.sample(10)koszt podejście 573.4ms.

wprowadź opis obrazu tutaj

Uwaga: moja tabela ma tylko 120 000 użytkowników. Im więcej masz płyt, tym większa będzie różnica w wydajności.


AKTUALIZACJA:

Wykonaj na stole zawierającym 550 000 wierszy

  1. Model.where(id: Model.pluck(:id).sample(10)) koszt 1384.0ms

wprowadź opis obrazu tutaj

  1. gem: quick_random_recordstylko kosztować 6.4mscałkowicie

wprowadź opis obrazu tutaj

Derek Fan
źródło
-2

Bardzo łatwy sposób na pobranie wielu losowych rekordów z tabeli. To daje 2 tanie zapytania.

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

Możesz zmienić liczbę „3” na wybraną liczbę losowych rekordów.

Arcolye
źródło
1
nie, część Model.pluck (: id) .sample (3) nie jest tania. Odczyta pole id dla każdego elementu w tabeli.
Maximiliano Guzman,
Czy istnieje szybszy sposób niezależny od bazy danych?
Arcolye,
-5

Właśnie napotkałem ten problem podczas tworzenia małej aplikacji, w której chciałem wybrać losowe pytanie z mojej bazy danych. Użyłem:

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

I to działa dobrze dla mnie. Nie mogę mówić o wydajności większych baz danych, ponieważ jest to tylko mała aplikacja.

rails_newbie
źródło
Tak, to po prostu pobranie wszystkich twoich rekordów i użycie na nich metod tablic ruby. Wadą jest oczywiście to, że oznacza to załadowanie wszystkich rekordów do pamięci, następnie losowe ich ponowne uporządkowanie, a następnie pobranie drugiej pozycji w uporządkowanej tablicy. To z pewnością może być świrem pamięci, jeśli masz do czynienia z dużym zbiorem danych. Pomijając drobne, dlaczego nie złapać pierwszego elementu? (tj. shuffle[0])
Andrew
must be shuffle [0]
Marcelo Austria