Record casuale in ActiveRecord

Ho bisogno di ottenere un record casuale da un tavolo tramite ActiveRecord. Ho seguito l’esempio di Jamis Buck del 2006 .

Tuttavia, ho anche trovato un altro modo tramite una ricerca su Google (imansible attribuire un collegamento a causa di nuove restrizioni dell’utente):

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

Sono curioso di sapere come gli altri qui lo hanno fatto o se qualcuno sa come sarebbe più efficiente.

Non ho trovato un modo ideale per farlo senza almeno due domande.

Quanto segue utilizza un numero generato casualmente (fino al conteggio dei record corrente) come offset .

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

Per essere onesti, ho appena utilizzato ORDER BY RAND () o RANDOM () (a seconda del database). Non si tratta di un problema di prestazioni se non si dispone di un problema di prestazioni.

In Rails 4 e 5 , usando Postgresql o SQLite , usando RANDOM() :

 Model.order("RANDOM()").first 

Presumibilmente la stessa cosa funzionerebbe per MySQL con RAND()

 Model.order("RAND()").first 

Questo è circa 2,5 volte più veloce dell’approccio nella risposta accettata .

Avvertenza : questo è lento per dataset di grandi dimensioni con milioni di record, quindi potresti voler aggiungere una clausola limit .

Il codice di esempio inizierà a comportarsi in modo non corretto una volta cancellati i record (favorirà ingiustamente gli articoli con ID inferiori)

Probabilmente stai meglio usando i metodi casuali all’interno del tuo database. Questi variano a seconda del DB che stai usando, ma: order => “RAND ()” funziona per mysql e: order => “RANDOM ()” funziona per postgres

 Model.first(:order => "RANDOM()") # postgres example 

Benchmarking di questi due metodi su MySQL 5.1.49, Ruby 1.9.2p180 su una tabella prodotti con + 5 milioni di record:

 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) 

L’offset in MySQL sembra essere molto più lento.

EDIT ho anche provato

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

Ma ho dovuto ucciderlo dopo ~ 60 secondi. MySQL era “Copia in tmp table su disco”. Non funzionerà.

Non deve essere così difficile.

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

pluck restituisce una matrice di tutti gli id ​​nella tabella. Il metodo di sample sull’array restituisce un ID casuale dall’array.

Questo dovrebbe funzionare bene, con uguale probabilità di selezione e supporto per le tabelle con righe cancellate. Puoi persino mescolarlo con vincoli.

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

Quindi scegli un utente a caso a cui piacciono i venerdì piuttosto che a qualsiasi utente.

Ho creato un rails 3 gem per gestirlo:

https://github.com/spilliton/randumb

Ti permette di fare cose del genere:

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

Non è consigliabile che tu usi questa soluzione, ma se per qualche ragione vuoi davvero selezionare un record casualmente mentre fai una sola query al database, puoi usare il metodo di sample dalla class Ruby Array , che ti permette di selezionare un caso object da un array.

 Model.all.sample 

Questo metodo richiede solo la query del database, ma è notevolmente più lento di alternative come Model.offset(rand(Model.count)).first che richiedono due query di database, sebbene quest’ultima sia ancora preferita.

Lo uso così spesso dalla console Estrapolo ActiveRecord in un inizializzatore – Esempio Rails 4:

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

Posso quindi chiamare Foo.random per riportare un record casuale.

Una query in Postgres:

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

Usando un offset, due domande:

 offset = rand(User.count) # returns an integer between 0 and (User.count - 1) Model.offset(offset).limit(1) 

Leggere tutti questi non mi ha dato molta fiducia su quale di questi avrebbe funzionato meglio nella mia particolare situazione con Rails 5 e MySQL / Maria 5.5. Così ho provato alcune risposte su ~ 65000 record e ho due take away:

  1. RAND () con un limit è un chiaro vincitore.
  2. Non usare 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) 

Questa risposta sintetizza, convalida e aggiorna la risposta di Mohamed , così come il commento di Nami WANG sullo stesso e il commento di Florian Pilz sulla risposta accettata – si prega di inviare voti a loro!

Se è necessario selezionare alcuni risultati casuali nell’ambito specificato :

 scope :male_names, -> { where(sex: 'm') } number_of_results = 10 rand = Names.male_names.pluck(:id).sample(number_of_results) Names.where(id: rand) 

È ansible utilizzare l’ sample metodo Array , l’ sample del metodo restituisce un object casuale da un array, per poterlo utilizzare è sufficiente eseguire una semplice query ActiveRecord che restituisce una raccolta, ad esempio:

 User.all.sample 

restituirà qualcosa di simile a questo:

 # 

Il metodo Ruby per il prelievo casuale di un elemento da un elenco è sample . Volendo creare un sample efficiente per ActiveRecord e basato sulle risposte precedenti, ho utilizzato:

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

Lo metto in lib/ext/sample.rb e poi lo carico con questo in config/initializers/monkey_patches.rb :

 Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file } 

Questa sarà una query se la dimensione del modello è già memorizzata nella cache e due in caso contrario.

Rails 4.2 e Oracle :

Per Oracle è ansible impostare un ambito sul modello in questo modo:

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

o

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

E poi per un esempio chiamalo così:

 Model.random_order.take(10) 

o

 Model.random_order.limit(5) 

ovviamente potresti anche effettuare un ordine senza un ambito come questo:

 Model.all.order('DBMS_RANDOM.RANDOM') # or DBMS_RANDOM.VALUE respectively 

Per il database MySQL, provare: Model.order (“RAND ()”). First

Se stai utilizzando PostgreSQL 9.5+, puoi sfruttare TABLESAMPLE per selezionare un record casuale.

I due metodi di campionamento predefiniti ( SYSTEM e BERNOULLI ) richiedono che BERNOULLI specificato il numero di righe da restituire come percentuale del numero totale di righe nella tabella.

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

Ciò richiede conoscere la quantità di record nella tabella per selezionare la percentuale appropriata, che potrebbe non essere facile da trovare rapidamente. Fortunatamente, esiste il modulo tsm_system_rows che consente di specificare il numero di righe da restituire direttamente.

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

Per utilizzare questo all’interno di ActiveRecord, abilitare innanzitutto l’estensione all’interno di una migrazione:

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

Quindi modifica la clausola from della query:

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

Non so se il metodo di campionamento SYSTEM_ROWS sarà del tutto casuale o se restituisce solo la prima riga da una pagina a caso.

La maggior parte di queste informazioni è stata presa da un post sul blog 2ndQuadrant scritto da Gulcin Yildirim .

Dopo aver visto così tante risposte ho deciso di confrontarle tutte sul mio database PostgreSQL (9.6.3). Io uso un tavolo più piccolo di 100.000 e mi sono liberato del Model.order (“RANDOM ()”) prima dato che era già più lento di due ordini di grandezza.

Usando una tabella con 2.500.000 voci con 10 colonne, il vincitore è stato il metodo pluck che è quasi 8 volte più veloce del runner up (offset, l’ho fatto solo su un server locale in modo tale che il numero potrebbe essere gonfiato ma è più grande di quello metodo è quello che finirò per usare. Vale anche la pena notare che questo potrebbe causare problemi è che pizzica più di 1 risultato alla volta poiché ognuno di questi sarà unico alias meno casuale.

Pluck vince correndo 100 volte sulla mia tabella di 25.000.000 di righe Modifica: in realtà questa volta include il guadagno nel ciclo se lo tolgo, viene eseguito con la stessa rapidità della semplice iterazione sull’ID. Però; occupa una discreta quantità di 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) 

Ecco i dati che eseguono 2000 volte sulla mia tabella di 100.000 righe per escludere casualmente

 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) 

Consigliamo vivamente questa gem per i record casuali, che è appositamente progettata per la tabella con molte righe di dati:

https://github.com/haopingfan/quick_random_records

Tutte le altre risposte si comportano male con un database di grandi dimensioni, tranne questa gem:

  1. quick_random_records costa solo 4.6ms totalmente.

inserisci la descrizione dell'immagine qui

  1. il User.order('RAND()').limit(10) costa 733.0ms .

inserisci la descrizione dell'immagine qui

  1. l’approccio offset risposta accettato costa in totale 245.4ms .

inserisci la descrizione dell'immagine qui

  1. l’ User.all.sample(10) costo di 573.4ms .

inserisci la descrizione dell'immagine qui


Nota: la mia tabella ha solo 120.000 utenti. Più record hai, più enorme sarà la differenza di prestazioni.

Sono nuovo di zecca per RoR, ma ho ottenuto questo per me:

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

Veniva da:

Come ordinare casualmente (scramble) un array in Ruby?

Cosa fare:

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

Per me è molto chiaro

Provo questo esempio di Sam sulla mia app usando le rotaie 4.2.8 di Benchmark (ho messo 1.Category.count per random, perché se il random prende uno 0 produrrà un errore (ActiveRecord :: RecordNotFound: Imansible trovare Categoria con ‘id’ = 0)) e la miniera era:

  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) 

.order('RANDOM()').limit(limit) sembra pulito ma è lento per le tabelle di grandi dimensioni perché ha bisogno di recuperare e ordinare tutte le righe anche se il limit è 1 (internamente nel database ma non in Rails). Non sono sicuro di MySQL ma questo succede in Postgres. Altre spiegazioni qui e qui .

Una soluzione per tabelle grandi è .from("products TABLESAMPLE SYSTEM(0.5)") dove 0.5 significa 0.5% . Tuttavia, trovo questa soluzione ancora lenta se hai WHERE condizioni che filtrano un sacco di righe. Immagino sia perché TABLESAMPLE SYSTEM(0.5) preleva tutte le righe prima che si applichino le condizioni WHERE .

Un’altra soluzione per tabelle di grandi dimensioni (ma non molto casuale) è:

 products_scope.limit(sample_size).sample(limit) 

dove sample_size può essere 100 (ma non troppo grande altrimenti è lento e consuma molta memoria), e il limit può essere 1 . Nota che sebbene questo sia veloce ma non è davvero casuale, è casuale solo nei record sample_size .

PS: i risultati del benchmark nelle risposte di cui sopra non sono affidabili (almeno in Postgres) perché alcune query DB in esecuzione in 2a volta possono essere notevolmente più veloci rispetto all’esecuzione al primo tempo, grazie alla cache del DB. E sfortunatamente non esiste un modo semplice per distriggersre la cache in Postgres per rendere questi benchmark affidabili.