Losowy rekord w ActiveRecord

151

Potrzebuję losowego rekordu z tabeli za pośrednictwem ActiveRecord. Wziąłem za przykładem Jamisa Bucka z 2006 roku .

Jednak natknąłem się również na inny sposób przez wyszukiwarkę Google (nie mogę przypisać linku z powodu nowych ograniczeń użytkowników):

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

Ciekaw jestem, jak zrobili to inni tutaj lub czy ktoś wie, jaki sposób byłby bardziej wydajny.

jyunderwood
źródło
2
2 punkty, które mogą pomóc w odpowiedzi. 1. Jak równomiernie rozmieszczone są Twoje identyfikatory, czy są one sekwencyjne? 2. Jak losowe musi być? Wystarczająco dobre, czy naprawdę losowe?
Michael
Są to identyfikatory sekwencyjne, które są automatycznie generowane przez activerecord i po prostu muszą być wystarczająco dobre.
jyunderwood
1
W takim razie proponowane przez Ciebie rozwiązanie jest bliskie ideału :) Użyłbym „SELECT MAX (id) FROM nazwa_tabeli” zamiast COUNT (*), ponieważ troszkę lepiej poradzi sobie z usuniętymi wierszami, w przeciwnym razie reszta jest w porządku. Krótko mówiąc, jeśli „wystarczająco dobre” jest w porządku, wystarczy mieć metodę, która zakłada dystrybucję zbliżoną do tego, co faktycznie masz. Jeśli jest jednolity, a nawet jak powiedziałeś, prosty rand działa świetnie.
Michael
1
To nie zadziała, jeśli usuniesz wiersze.
Venkat D.

Odpowiedzi:

136

Nie znalazłem idealnego sposobu na zrobienie tego bez co najmniej dwóch zapytań.

W poniższym przykładzie zastosowano losowo wygenerowaną liczbę (aż do bieżącej liczby rekordów) jako przesunięcie .

offset = rand(Model.count)

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

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

Szczerze mówiąc, właśnie używałem ORDER BY RAND () lub RANDOM () (w zależności od bazy danych). Nie jest to problem z wydajnością, jeśli nie masz problemu z wydajnością.

Toby Hede
źródło
2
Kod Model.find(:offset => offset).firstzgłosi błąd. Myślę, że Model.first(:offset => offset)może działać lepiej.
Harish Shetty
1
tak, pracowałem z Railsami 3 i wciąż nie rozumiem formatów zapytań między wersjami.
Toby Hede
7
Zauważ, że użycie przesunięcia jest bardzo powolne w przypadku dużego zbioru danych, ponieważ w rzeczywistości wymaga skanowania indeksu (lub skanowania tabeli, w przypadku gdy indeks klastrowy jest używany jak InnoDB). Innymi słowy, jest to operacja O (N), ale „WHERE id> = # {rand_id} ORDER BY id ASC LIMIT 1” to O (log N), co jest znacznie szybsze.
kenn
15
Należy pamiętać, że podejście offsetowe daje tylko jeden losowo znaleziony punkt danych (pierwszy, wszystkie następne są nadal sortowane według id). Jeśli potrzebujesz wielu losowo wybranych rekordów, musisz użyć tego podejścia wielokrotnie lub skorzystać z metody kolejności losowej dostarczonej przez Twoją bazę danych, tj. Thing.order("RANDOM()").limit(100)Dla 100 losowo wybranych wpisów. (Pamiętaj, że jest RANDOM()w PostgreSQL i RAND()MySQL ... nie jest tak przenośny, jak byś chciał).
Florian Pilz
3
Nie działa dla mnie na Railsach 4. Użyj Model.offset(offset).first.
mahemoff
206

Szyny 6

Jak stwierdził Jason w komentarzach, w Railsach 6 argumenty niebędące atrybutami są niedozwolone. Musisz opakować wartość w Arel.sql()instrukcję.

Model.order(Arel.sql('RANDOM()')).first

Szyny 5, 4

W Railsach 4 i 5 , używając Postgresql lub SQLite , używając RANDOM():

Model.order('RANDOM()').first

Przypuszczalnie to samo działałoby dla MySQL zRAND()

Model.order('RAND()').first

To około 2,5 raza szybciej niż podejście w przyjętej odpowiedzi .

Ostrzeżenie : jest to powolne w przypadku dużych zbiorów danych z milionami rekordów, więc warto dodać limitklauzulę.

Mohamad
źródło
4
„Random ()” działa również w sqlite, więc dla tych z nas, którzy wciąż rozwijają się w sqlite i używają postgresów w środowisku produkcyjnym, twoje rozwiązanie działa w obu środowiskach.
wuliwong
5
Stworzyłem dla tego punkt odniesienia w stosunku do zaakceptowanej odpowiedzi. W Postgresql 9.4 podejście tej odpowiedzi jest około dwa razy szybsze.
panmari
3
Wygląda na to, że nie jest to zalecane na mysql webtrenches.com/post.cfm/avoid-rand-in-mysql
Prakash Murthy
To najszybsze rozwiązanie
Sergio Belevskij
1
"Argumenty niebędące atrybutami będą niedozwolone w Railsach 6.0. Ta metoda nie powinna być wywoływana z wartościami podanymi przez użytkownika, takimi jak parametry żądania lub atrybuty modelu. Znane bezpieczne wartości można przekazać, opakowując je w Arel.sql ()."
Trenton Tyler
73

Twój przykładowy kod zacznie działać niepoprawnie po usunięciu rekordów (będzie nieuczciwie faworyzował elementy o niższych identyfikatorach)

Prawdopodobnie lepiej będzie, jeśli użyjesz losowych metod w swojej bazie danych. Różnią się one w zależności od używanej bazy danych, ale: order => "RAND ()" działa dla mysql i: order => "RANDOM ()" działa dla postgres

Model.first(:order => "RANDOM()") # postgres example
semanticart
źródło
7
ORDER BY RAND () dla MySQL kończy się w przerażającym czasie wykonywania, gdy wzrasta ilość danych. Jest nie do utrzymania (w zależności od wymagań czasowych), nawet zaczynając od zaledwie tysięcy rzędów.
Michael
Michael porusza świetną kwestię (dotyczy to również innych DB). Generalnie wybieranie losowych wierszy z dużych tabel nie jest czymś, co chcesz robić w dynamicznej akcji. Buforowanie to twój przyjaciel. Ponowne przemyślenie tego, co próbujesz osiągnąć, również może nie być złym pomysłem.
semanticart
1
Porządkowanie RAND () w mysql w tabeli z około milionem wierszy to slooooooooooooooooooow.
Subimage
24
Już nie działa. Użyj Model.order("RANDOM()").firstzamiast tego.
phil pirozhkov
Wolny i specyficzny dla bazy danych. ActiveRecord ma działać bezproblemowo między bazami danych, więc nie powinieneś używać tej metody.
Dex
29

Test porównawczy tych dwóch metod w MySQL 5.1.49, Ruby 1.9.2p180 na tabeli produktów z + 5 milionami rekordów:

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)

Offset w MySQL wydaje się być znacznie wolniejszy.

EDYCJA Próbowałem też

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

Ale musiałem go zabić po ~ 60 sekundach. MySQL było „Kopiowanie do tabeli tmp na dysku”. To nie zadziała.

dkam
źródło
1
Dla tych, którzy szukają więcej testów, jak długo trwa prawdziwie losowe podejście: próbowałem Thing.order("RANDOM()").firstna tabeli z 250 tys. Wpisów - zapytanie zakończyło się poniżej pół sekundy. (PostgreSQL 9.0, REE 1.8.7, 2 rdzenie 2,66 GHz) To dla mnie wystarczająco szybkie, ponieważ przeprowadzam jednorazowe „czyszczenie”.
Florian Pilz
6
Metoda rand Rubiego zwraca o jeden mniej od podanej liczby, więc będziesz chciał, w rand_id = rand(Product.count) + 1przeciwnym razie nigdy nie otrzymasz ostatniego rekordu.
Ritchie
4
Notatka random1nie będzie działać, jeśli kiedykolwiek usuniesz wiersz w tabeli. (Liczba będzie mniejsza niż maksymalny identyfikator i nigdy nie będzie można wybrać wierszy z wysokimi identyfikatorami).
Nicholas,
Użycie random2można poprawić, #orderużywając kolumny indeksowanej.
Carson Reinke,
18

To nie musi być takie trudne.

ids = Model.pluck(:id)
random_model = Model.find(ids.sample)

pluckzwraca tablicę wszystkich identyfikatorów w tabeli. sampleSposób na tablicy powraca losowy identyfikator z tablicy.

Powinno to działać dobrze, z równym prawdopodobieństwem wyboru i obsługi tabel z usuniętymi wierszami. Możesz nawet mieszać to z ograniczeniami.

User.where(favorite_day: "Friday").pluck(:id)

W ten sposób wybierz przypadkowego użytkownika, który lubi piątki, a nie dowolnego użytkownika.

Niels B.
źródło
8
To jest czyste i działa przy małym stole lub jednorazowym użyciu, po prostu pamiętaj, że nie będzie skalować. Na stole 3M wyrywanie identyfikatorów zajmuje mi około 15 sekund na MariaDB.
mahemoff,
2
Trafne spostrzeżenie. Czy znalazłeś alternatywne rozwiązanie, które jest szybsze, zachowując te same cechy?
Niels B.
Czy przyjęte rozwiązanie offsetowe nie zachowuje tych samych właściwości?
mahemoff
Nie, nie obsługuje warunków i nie ma równego prawdopodobieństwa wyboru dla tabel z usuniętymi rekordami.
Niels B.
1
Pomyśl o tym, jeśli zastosujesz wiązania zarówno podczas liczenia, jak i wybierania z przesunięciem, technika powinna działać. Wyobrażałem sobie, że zastosuję go tylko do liczenia.
Niels B.
15

Nie zaleca się korzystania z tego rozwiązania, ale jeśli z jakiegoś powodu naprawdę chcesz losowo wybrać rekord podczas wykonywania tylko jednego zapytania do bazy danych, możesz skorzystać z samplemetody z klasy Ruby Array , która pozwala na wybranie losowego elementu z tablicy.

Model.all.sample

Ta metoda wymaga tylko zapytania do bazy danych, ale jest znacznie wolniejsza niż alternatywy, takie jak Model.offset(rand(Model.count)).firstwymagające dwóch zapytań do bazy danych, chociaż ta ostatnia jest nadal preferowana.

Ryan Atallah
źródło
99
Nie rób tego. Zawsze.
Zabba
5
Jeśli masz w bazie danych 100 000 wierszy, wszystkie z nich musiałyby zostać załadowane do pamięci.
Venkat D.
3
Oczywiście nie jest to zalecane do produkcji kodu czasu rzeczywistego, ale podoba mi się to rozwiązanie, jest bardzo jasne, aby używać go w specjalnych sytuacjach, takich jak wypełnianie bazy danych fałszywymi wartościami.
fguillen
13
Proszę - nigdy nie mów nigdy. Jest to świetne rozwiązanie do debugowania w czasie programowania, jeśli tabela jest mała. (A jeśli pobierasz próbki, prawdopodobnie debugowanie jest przypadkiem użycia).
mahemoff
Używam do wysiewu i jest dla mnie dobry. Dodatkowo działa Model.all.sample (n) :)
Arnaldo Ignacio Gaspar Véjar
13

Zrobiłem klejnot szyn 3, aby sobie z tym poradzić:

https://github.com/spilliton/randumb

Pozwala robić takie rzeczy:

Model.where(:column => "value").random(10)
spilliton
źródło
7
W dokumentacji tego klejnotu wyjaśniają, że „randumb po prostu dodaje dodatkowe ORDER BY RANDOM()(lub RAND()dla mysql) do zapytania”. - dlatego komentarze dotyczące złego działania wymienione w komentarzach do odpowiedzi @semanticart obowiązują również podczas korzystania z tego klejnotu. Ale przynajmniej jest niezależny od DB.
Nicolas,
8

Używam tego tak często z konsoli, że rozszerzam ActiveRecord w inicjatorze - przykład Rails 4:

class ActiveRecord::Base
  def self.random
    self.limit(1).offset(rand(self.count)).first
  end
end

Mogę wtedy zadzwonić Foo.randomi przynieść losowy rekord.

Knotty66
źródło
1
potrzebujesz limit(1)? ActiveRecord#firstpowinien być wystarczająco inteligentny, aby to zrobić.
tokland
6

Jedno zapytanie w Postgres:

User.order('RANDOM()').limit(3).to_sql # Postgres example
=> "SELECT "users".* FROM "users" ORDER BY RANDOM() LIMIT 3"

Używając przesunięcia, dwa zapytania:

offset = rand(User.count) # returns an integer between 0 and (User.count - 1)
Model.offset(offset).limit(1)
Thomas Klemm
źródło
1
Nie ma potrzeby -1, rand liczy się do num - 1
anemaria20
Dzięki, zmienione: +1:
Thomas Klemm
5

Przeczytanie ich wszystkich nie dało mi wiele pewności, które z nich najlepiej sprawdzą się w mojej konkretnej sytuacji z Railsami 5 i MySQL / Maria 5.5. Przetestowałem więc niektóre odpowiedzi na ~ 65000 rekordach i mam dwa wnioski:

  1. RAND () z a limitjest wyraźnym zwycięzcą.
  2. Nie używaj 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)

Ta odpowiedź syntetyzuje, weryfikuje i aktualizuje odpowiedź Mohameda , a także komentarz Nami WANG na ten sam temat oraz komentarz Floriana Pilza na temat zaakceptowanej odpowiedzi - prosimy o przesyłanie im głosów!

Sam
źródło
3

Możesz użyć Arraymetody sample, metoda samplezwraca losowy obiekt z tablicy, aby go użyć wystarczy wykonać w prostym ActiveRecordzapytaniu zwracającym kolekcję, na przykład:

User.all.sample

zwróci coś takiego:

#<User id: 25, name: "John Doe", email: "[email protected]", created_at: "2018-04-16 19:31:12", updated_at: "2018-04-16 19:31:12">
trejo08
źródło
Nie polecałbym pracy z metodami tablicowymi podczas korzystania z AR. W ten sposób zajmuje prawie 8 razy więcej czasu na wykonanie order('rand()').limit(1)tej samej pracy (przy ~ 10K rekordów).
Sebastian Palma,
3

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 733.0ms.

wprowadź opis obrazu tutaj

  1. zaakceptowana odpowiedź offsetkosztowała 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.

Derek Fan
źródło
2

Jeśli chcesz wybrać losowe wyniki w określonym zakresie :

scope :male_names, -> { where(sex: 'm') }
number_of_results = 10

rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)
Jurij Karpowicz
źródło
1

Metoda Ruby do losowego wybierania pozycji z listy to sample. Chcąc stworzyć wydajny sampledla ActiveRecord i na podstawie poprzednich 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 }

Będzie to jedno zapytanie, jeśli rozmiar modelu jest już buforowany, a dwa w innym przypadku.

Dan Kohn
źródło
1

Rails 4.2 i Oracle :

W przypadku Oracle możesz ustawić zakres na swoim modelu w następujący sposób:

scope :random_order, -> {order('DBMS_RANDOM.RANDOM')}

lub

scope :random_order, -> {order('DBMS_RANDOM.VALUE')}

A potem dla próbki nazwij to tak:

Model.random_order.take(10)

lub

Model.random_order.limit(5)

oczywiście możesz też złożyć zamówienie bez zakresu:

Model.all.order('DBMS_RANDOM.RANDOM') # or DBMS_RANDOM.VALUE respectively
Mahatmanich
źródło
Możesz to zrobić z postgres z order('random()'i MySQL z order('rand()'). To zdecydowanie najlepsza odpowiedź.
jrochkind
1

W przypadku bazy danych MySQL spróbuj najpierw: Model.order ("RAND ()")

Vadim Eremeev
źródło
To nie działa na mysql .. powinieneś zawrzeć przynajmniej z jakim silnikiem DB ma to współpracować
Arnold Roa
Przepraszamy, wystąpiła literówka. Naprawiono teraz. Powinno działać dla mysql (tylko)
Vadim Eremeev
1

Jeśli używasz PostgreSQL 9.5+, możesz TABLESAMPLEwybrać losowy rekord.

Dwie domyślne metody próbkowania ( SYSTEMi BERNOULLI) wymagają określenia liczby wierszy do zwrócenia jako procent całkowitej liczby wierszy w tabeli.

-- Fetch 10% of the rows in the customers table.
SELECT * FROM customers TABLESAMPLE BERNOULLI(10);

Wymaga to znajomości ilości rekordów w tabeli, aby wybrać odpowiednią wartość procentową, co może nie być łatwe do szybkiego znalezienia. Na szczęście istnieje tsm_system_rowsmoduł, który pozwala określić liczbę wierszy do bezpośredniego zwrotu.

CREATE EXTENSION tsm_system_rows;

-- Fetch a single row from the customers table.
SELECT * FROM customers TABLESAMPLE SYSTEM_ROWS(1);

Aby użyć tego w ActiveRecord, najpierw włącz rozszerzenie w ramach migracji:

class EnableTsmSystemRowsExtension < ActiveRecord::Migration[5.0]
  def change
    enable_extension "tsm_system_rows"
  end
end

Następnie zmodyfikuj fromklauzulę zapytania:

customer = Customer.from("customers TABLESAMPLE SYSTEM_ROWS(1)").first

Nie wiem, czy SYSTEM_ROWSmetoda próbkowania będzie całkowicie losowa, czy po prostu zwróci pierwszy wiersz z losowej strony.

Większość tych informacji pochodzi z posta na blogu 2ndQuadrant napisanego przez Gulcina Yildirima .

Adam Sheehan
źródło
1

Po zobaczeniu tak wielu odpowiedzi zdecydowałem się je wszystkie porównać w mojej bazie danych PostgreSQL (9.6.3). Używam mniejszej tabeli 100 000 i najpierw pozbyłem się Model.order („RANDOM ()”), ponieważ był już o dwa rzędy wielkości wolniejszy.

Korzystając z tabeli z 2500000 wpisów z 10 kolumnami, zwycięzcą była metoda zrywania, która była prawie 8 razy szybsza niż druga osoba (przesunięcie. Uruchomiłem to tylko na lokalnym serwerze, więc liczba może być zawyżona, ale jest na tyle większa, że metoda jest tym, czego ostatecznie użyję. Warto również zauważyć, że może to powodować problemy, jeśli wyrywasz więcej niż 1 wynik na raz, ponieważ każdy z nich będzie niepowtarzalny, czyli mniej losowy.

Pluck wygrywa z uruchomieniem 100 razy na mojej tabeli zawierającej 25 000 000 wierszy Edytuj: w rzeczywistości ten czas obejmuje zrywanie w pętli, jeśli to wyciągnę, działa mniej więcej tak szybko, jak prosta iteracja na id. Jednak; zajmuje sporo pamięci RAM.

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)

Oto dane działające 2000 razy w mojej tabeli zawierającej 100 000 wierszy, aby wykluczyć losowość

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)
Mendoza
źródło
1

Bardzo stare pytanie, ale z:

rand_record = Model.all.shuffle

Masz tablicę rekordów, posortuj według losowej kolejności. Nie potrzebujesz klejnotów ani skryptów.

Jeśli chcesz jeden rekord:

rand_record = Model.all.shuffle.first
Gregdebrick
źródło
1
Nie jest to najlepsza opcja, ponieważ powoduje to załadowanie wszystkich rekordów do pamięci. Ponadto shuffle.first==.sample
Andrew Rozhenko
0

Jestem zupełnie nowy w RoR, ale udało mi się to:

 def random
    @cards = Card.all.sort_by { rand }
 end

To przyszło z:

Jak losowo posortować (wymieszać) tablicę w Rubim?

Aaron Pennington
źródło
4
Złe jest to, że ładuje wszystkie karty z bazy danych. Bardziej efektywnie jest to zrobić w bazie danych.
Anton Kuzmin
Możesz także tasować tablice za pomocą array.shuffle. W każdym razie uważaj, ponieważ Card.allzaładują wszystkie rekordy karty do pamięci, co staje się mniej wydajne, im więcej obiektów, o których mówimy.
Thomas Klemm
0

Co do zrobienia:

rand_record = Model.find(Model.pluck(:id).sample)

Dla mnie jest dużo jasne

poramo
źródło
0

Próbuję tego z przykładu Sama w mojej aplikacji przy użyciu rails 4.2.8 Benchmark (wstawiam 1..Category.count dla losowego, ponieważ jeśli losowa przyjmuje 0, spowoduje to błąd (ActiveRecord :: RecordNotFound: Could not find Kategoria z 'id' = 0)), a moja:

 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)
rld
źródło
0

.order('RANDOM()').limit(limit)wygląda schludnie, ale działa wolno w przypadku dużych tabel, ponieważ musi pobrać i posortować wszystkie wiersze, nawet jeśli limitma wartość 1 (wewnętrznie w bazie danych, ale nie w Railsach). Nie jestem pewien co do MySQL, ale dzieje się tak w Postgres. Więcej wyjaśnień tutaj i tutaj .

Jednym z rozwiązań dla dużych stołów jest .from("products TABLESAMPLE SYSTEM(0.5)")to, co 0.5oznacza 0.5%. Jednak uważam, że to rozwiązanie jest nadal powolne, jeśli masz WHEREwarunki, które odfiltrowują wiele wierszy. Wydaje mi się, że to dlatego, że wcześniej TABLESAMPLE SYSTEM(0.5)pobierz wszystkie wierszeWHERE zastosowaniem warunków.

Innym rozwiązaniem dla dużych tabel (ale niezbyt losowych) jest:

products_scope.limit(sample_size).sample(limit)

gdzie sample_sizemoże być 100(ale nie za duży, w przeciwnym razie jest powolny i zużywa dużo pamięci) i limitmoże być 1. Zauważ, że chociaż jest to szybkie, ale nie jest to przypadkowe, jest losowe sample_sizetylko w rekordach.

PS: Wyniki testów porównawczych w powyższych odpowiedziach nie są wiarygodne (przynajmniej w Postgres), ponieważ niektóre zapytania DB uruchomione po raz drugi mogą być znacznie szybsze niż uruchomione za pierwszym razem, dzięki pamięci podręcznej DB. I niestety nie ma łatwego sposobu na wyłączenie pamięci podręcznej w Postgres, aby te testy były wiarygodne.

Linh Dam
źródło
0

Wraz z użyciem RANDOM()możesz również wrzucić to do zakresu:

class Thing
  scope :random, -> (limit = 1) {
    order('RANDOM()').
    limit(limit)
  }
end

Lub, jeśli nie podoba ci się to jako zakres, po prostu wrzuć to do metody klasowej. Teraz Thing.randomdziała razem z Thing.random(n).

Damien Roche
źródło
0

W zależności od znaczenia słowa „losowe” i tego, co faktycznie chcesz zrobić, takemoże wystarczyć.

Przez „znaczenie” losowości mam na myśli:

  • Masz na myśli jakiś element, nie obchodzi mnie jego pozycja? to wystarczy.
  • Teraz, jeśli masz na myśli „podaj mi dowolny element z dużym prawdopodobieństwem, że powtarzane eksperymenty dadzą mi inne elementy z zestawu”, wymuś „Szczęście” dowolną z metod wymienionych w innych odpowiedziach.

Na przykład do testowania dane próbne i tak mogły zostać utworzone losowo, więc takejest więcej niż wystarczające, a szczerze mówiąc, nawet first.

https://guides.rubyonrails.org/active_record_querying.html#take

jgomo3
źródło