Rails 3: Ottieni record casuali


132

Quindi, ho trovato diversi esempi per trovare un record casuale in Rails 2 - il metodo preferito sembra essere:

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

Essendo qualcosa di nuovo, non sono sicuro di come questo possa essere costruito usando la nuova sintassi find in Rails 3.

Allora, qual è il "Rails 3 Way" per trovare un disco casuale?



9
^^ tranne che sto specificamente cercando il modo ottimale di Rails 3, che è l'intero scopo della domanda.
Andrew

rails 3 specifico è solo una catena di query :)
fl00r

Risposte:


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

o

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

In realtà, in Rails 3 funzioneranno tutti gli esempi. Ma usare l'ordine RANDOMè piuttosto lento per i tavoli più grandi ma più in stile sql

UPD. Puoi usare il seguente trucco su una colonna indicizzata (sintassi PostgreSQL):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;

11
Il tuo primo esempio non funzionerà in MySQL: la sintassi di MySQL è Thing.first (: order => "RAND ()") (un pericolo di scrivere SQL piuttosto che usare le astrazioni di ActiveRecord)
DanSingerman

@ DanSingerman, sì, è specifico per DB RAND()o RANDOM(). Grazie
fl00r

E questo non creerà problemi se ci sono elementi mancanti dall'indice? (se qualcosa nel mezzo dello stack viene eliminato, ci sarà la possibilità che venga richiesto?
Victor S

@VictorS, no, non #offset passa al prossimo record disponibile. L'ho provato con Ruby 1.9.2 e Rails 3.1
SooDesuNe l'

1
@JohnMerlino, sì 0 è offset, non id. Offet 0 indica il primo articolo in base all'ordine.
fl00r,

29

Sto lavorando a un progetto ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ) in cui il db è in localhost e la tabella degli utenti ha un po 'più di 100K record .

utilizzando

ordina per RAND ()

è abbastanza lento

User.order ( "RAND (id)"). Prima

diventa

SELEZIONA users. * usersDALL'ORDINE PER RAND (id) LIMIT 1

e impiega da 8 a 12 secondi per rispondere !!

Registro delle rotaie:

Carico utente (11030,8 ms) SELEZIONA users. * DA usersORDINE PER RAND () LIMIT 1

da mysql's spiega

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

Si può vedere che non viene utilizzato alcun indice ( possible_keys = NULL ), viene creata una tabella temporanea ed è necessario un passaggio aggiuntivo per recuperare il valore desiderato ( extra = Uso temporaneo; Uso di filesort ).

D'altra parte, suddividendo la query in due parti e usando Ruby, abbiamo un ragionevole miglioramento dei tempi di risposta.

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

(; zero per console)

Registro delle rotaie:

User Load (25.2ms) SELEZIONA ID DA usersUser Load (0.2ms) SELEZIONA users. * DA usersDOVE users. id= 106854 LIMITE 1

e mysql spiega spiega perché:

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

ora possiamo usare solo gli indici e la chiave primaria e fare il lavoro circa 500 volte più velocemente!

AGGIORNARE:

come sottolineato da icantbecool nei commenti, la soluzione di cui sopra presenta un difetto se nella tabella sono presenti record eliminati.

Una soluzione in questo può essere

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

che si traduce in due query

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

e funziona in circa 500ms.


l'aggiunta di ".id" dopo "last" al secondo esempio eviterà l'errore "Impossibile trovare il modello senza ID". Ad esempio User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine

Avvertimento! In MySQL NON ti RAND(id)verrà assegnato un ordine casuale diverso per ogni query. Utilizzare se si desidera un ordine diverso per ogni query. RAND()
Justin Tanner,

User.find (users.first (Random.rand (users.length)). Last.id) non funzionerà se è stato eliminato un record. [1,2,4,5,] e potenzialmente potrebbe scegliere l'id di 3, ma non ci sarebbe una relazione record attiva.
icantbecool,

Inoltre, users = User.scoped.select (: id); nil non è obsoleto. Usa questo invece: users = User.where (nil) .select (: id)
icantbecool

Credo che usare Random.rand (users.length) come parametro per primo sia un bug. Random.rand può restituire 0. Quando 0 viene utilizzato come parametro per primo, il limite viene impostato su zero e questo non restituisce alcun record. Quello che si dovrebbe usare invece è 1 + Casuale (utenti.lunghezza) assumendo utenti.lunghezza> 0.
SWoo

12

Se si utilizza Postgres

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

Se si utilizza MySQL

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

In entrambi i casi stai selezionando 5 record in modo casuale dalla tabella Users. Ecco la query SQL effettiva in visualizzata nella console.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

11

Ho fatto un gioiello di rails 3 per farlo che si comporta meglio su tavoli di grandi dimensioni e consente di concatenare relazioni e ambiti:

https://github.com/spilliton/randumb

(modifica): il comportamento predefinito della mia gemma utilizza fondamentalmente lo stesso approccio di cui sopra ora, ma hai la possibilità di utilizzare il vecchio modo se vuoi :)


6

Molte delle risposte pubblicate in realtà non funzioneranno bene su tabelle piuttosto grandi (1+ milioni di righe). L'ordinamento casuale richiede rapidamente alcuni secondi e anche fare un conteggio sul tavolo richiede abbastanza tempo.

Una soluzione che funziona bene per me in questa situazione è usare RANDOM()con una condizione dove:

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

Su una tabella con oltre un milione di righe, questa query richiede generalmente meno di 2ms.


Un altro vantaggio della tua soluzione è la takefunzione use che fornisce LIMIT(1)query ma restituisce un singolo elemento anziché un array. Quindi non abbiamo bisogno di invocarefirst
Piotr Galas il

Mi sembra che i record all'inizio della tabella abbiano una maggiore probabilità di essere selezionati in questo modo, il che potrebbe non essere quello che vuoi ottenere.
Il

5

eccoci qui

modo di rotaie

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

uso

Model.random #returns single random object

o il secondo pensiero è

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

utilizzo:

Model.random #returns shuffled collection

Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno,

se non ci sono utenti e vuoi ottenere 2, allora ricevi errori. ha senso.
Tim Kretschmer,

1
Il secondo approccio non funzionerà con Postgres, ma puoi usare "RANDOM()"invece ...
Daniel Richter

4

Questo mi è stato molto utile, tuttavia avevo bisogno di un po 'più di flessibilità, quindi è quello che ho fatto:

Caso 1: Trovare una fonte record casuale : trevor turk site
Aggiungi questo al modello 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

quindi nel controller puoi chiamare qualcosa del genere

@thing = Thing.random

Caso 2: Trovare più record casuali (senza ripetizioni) fonte: non ricordo che
avevo bisogno di trovare 10 record casuali senza ripetizioni, quindi questo è quello che ho trovato funzionato
nel tuo controller:

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

Questo troverà 10 record casuali, tuttavia vale la pena ricordare che se il database è particolarmente grande (milioni di record), questo non sarebbe l'ideale e le prestazioni saranno ostacolate. Si esibirà bene fino a qualche migliaio di dischi che era sufficiente per me.


4

Il metodo Ruby per selezionare casualmente un oggetto da un elenco è sample. Volendo creare un efficiente sampleper ActiveRecord e sulla base delle risposte precedenti, ho usato:

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

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

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

In realtà, #countfarà una chiamata al DB per a COUNT. Se il record è già caricato, questa potrebbe essere una cattiva idea. Un refactor sarebbe #sizeinvece usare in quanto deciderà se #countdeve essere usato o, se il record è già caricato, da usare #length.
BenMorganIO,

Passato da counta in sizebase al tuo feedback. Maggiori informazioni su: dev.mensfeld.pl/2014/09/…
Dan Kohn,

3

Funziona in Rails 5 ed è indipendente da DB:

Questo nel tuo controller:

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

Puoi, ovviamente, mettere questo in una preoccupazione come mostrato qui .

app / modelli / preoccupazioni / 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

poi...

app / modelli / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Quindi puoi usare semplicemente facendo:

Books.random

o

Books.random(3)

Questo richiede sempre i record successivi, che devono essere almeno documentati (poiché potrebbe non essere ciò che l'utente desidera).
Il

2

È possibile utilizzare sample () in ActiveRecord

Per esempio

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

Fonte: http://thinkingeek.com/2011/07/04/easily-select-random-records-rails/


33
Questa è una pessima query da utilizzare se si dispone di una grande quantità di record, poiché il DB selezionerà TUTTI i record, quindi Rails sceglierà cinque record da quello - enormemente dispendioso.
DaveStephens,

5
samplenon è in ActiveRecord, l'esempio è in array. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans

3
Questo è un modo costoso per ottenere un record casuale, soprattutto da una tabella di grandi dimensioni. Rails caricherà un oggetto per ogni record dalla tua tabella in memoria. Se hai bisogno di prove, esegui 'rails console', prova 'SomeModelFromYourApp.find (: all) .sample (5)' e guarda l'SQL prodotto.
Eliot Sykes,

1
Vedi la mia risposta, che trasforma questa risposta costosa in una bellezza ottimizzata per ottenere più record casuali.
Arcolye,

1

Se si utilizza Oracle

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

Produzione

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10

1

Consiglio vivamente questo gioiello per i record casuali, che è appositamente progettato per la tabella con molte righe di dati:

https://github.com/haopingfan/quick_random_records

Tutte le altre risposte funzionano male con database di grandi dimensioni, tranne questa gemma:

  1. quick_random_records costa solo 4.6mstotalmente.

inserisci qui la descrizione dell'immagine

  1. il User.order('RAND()').limit(10)costo di risposta accettato 733.0ms.

inserisci qui la descrizione dell'immagine

  1. l' offsetapproccio costa 245.4mstotalmente.

inserisci qui la descrizione dell'immagine

  1. il User.all.sample(10)costo di avvicinamento 573.4ms.

inserisci qui la descrizione dell'immagine

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


AGGIORNARE:

Esegui sul tavolo con 550.000 righe

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

inserisci qui la descrizione dell'immagine

  1. gem: quick_random_recordscosta solo 6.4mstotalmente

inserisci qui la descrizione dell'immagine


-2

Un modo molto semplice per ottenere più record casuali dalla tabella. Questo fa 2 domande a basso costo.

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

Puoi cambiare "3" con il numero di record casuali che desideri.


1
no, la parte Model.pluck (: id) .sample (3) non è economica. Leggerà il campo id per ogni elemento nella tabella.
Maximiliano Guzman,

Esiste un modo agnostico di database più veloce?
Arcolye,

-5

Ho appena riscontrato questo problema sviluppando una piccola applicazione in cui volevo selezionare una domanda casuale dal mio database. Ero solito:

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

E funziona bene per me. Non posso parlare di come le prestazioni per DB più grandi, poiché questa è solo una piccola applicazione.


Sì, questo sta solo ottenendo tutti i tuoi record e usando i metodi dell'array ruby ​​su di essi. Lo svantaggio è ovviamente che significa caricare tutti i tuoi record in memoria, quindi riordinarli casualmente, quindi afferrare il secondo elemento nella matrice riordinata. Questo potrebbe sicuramente essere un ostacolo alla memoria se si trattasse di un set di dati di grandi dimensioni. Minori a parte, perché non afferrare il primo elemento? (es. shuffle[0])
Andrew

must shuffle [0]
Marcelo Austria
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.