Enregistrement aléatoire dans ActiveRecord

j'ai besoin d'obtenir un enregistrement aléatoire à partir d'une table via ActiveRecord. J'ai suivi l'exemple de Jamis Buck de 2006 .

cependant, je suis également tombé sur une autre voie via une recherche Google (ne peut pas attribuer avec un lien En raison de nouvelles restrictions de l'utilisateur):

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

je suis curieux de savoir comment d'autres ici l'ont fait ou si quelqu'un sait de quelle manière serait plus efficace.

138
demandé sur user2262149 2010-05-02 06:11:52

22 réponses

Je n'ai pas trouvé le moyen idéal de le faire sans au moins deux requêtes.

ce qui suit utilise un nombre généré au hasard (jusqu'au nombre d'enregistrements courant) comme un offset .

offset = rand(Model.count)

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

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

pour être honnête, j'ai juste utilisé ORDER BY RAND() ou RANDOM() (selon la base de données). Ce n'est pas un problème de performances si vous n'avez pas de problème de performances.

125
répondu Toby Hede 2014-09-24 20:13:34

dans Rails 4 et 5 , en utilisant Postgresql ou SQLite , en utilisant RANDOM() :

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

probablement la même chose fonctionnerait pour MySQL avec RAND()

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

Ce est environ 2,5 fois plus vite que l'approche de la a accepté de répondre à .

Caveat : ceci est lent pour les grands ensembles de données avec des millions d'enregistrements, donc vous pourriez vouloir ajouter une clause limit .

152
répondu Mohamad 2018-04-28 19:23:51

votre code d'exemple commencera à se comporter de manière inexacte une fois les enregistrements supprimés (il favorisera injustement les éléments avec des identifiants inférieurs)

vous êtes probablement mieux en utilisant les méthodes aléatoires dans votre base de données. Ceux-ci varient en fonction de la base de données que vous utilisez, mais: order = > "RAND ()" fonctionne pour mysql et: order = > " RANDOM ()" fonctionne pour postgres

Model.first(:order => "RANDOM()") # postgres example
71
répondu semanticart 2010-05-02 02:40:45

Benchmarking these two methods on MySQL 5.1.49, Ruby 1.9.2p180 sur une table de produits avec + 5 millions d'enregistrements:

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 dans MySQL semble être beaucoup plus lent.

EDIT J'ai aussi essayé

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

mais j'ai dû le tuer après ~60 secondes. MySQL était "Copying to tmp table on disk". Qui ne va pas au travail.

27
répondu dkam 2011-05-10 01:12:43

ça ne doit pas être si dur.

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

pluck retourne un tableau de tous les id dans la table. La méthode sample sur le tableau, renvoie un id aléatoire du tableau.

cela devrait bien fonctionner, avec une probabilité égale de sélection et de soutien pour les tableaux avec des lignes supprimées. Vous pouvez même mélanger avec contraintes.

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

et de ce fait choisir un utilisateur aléatoire qui aime les vendredis plutôt que n'importe quel utilisateur.

17
répondu Niels B. 2014-03-30 14:35:05

j'ai fait un rail 3 gemme pour gérer cela:

https://github.com/spilliton/randumb

il permet de faire des choses comme ceci:

Model.where(:column => "value").random(10)
13
répondu spilliton 2012-05-18 02:26:22

il n'est pas conseillé que vous utilisez cette solution, mais si pour une raison quelconque vous vraiment voulez sélectionner un enregistrement au hasard tout en faisant une seule requête de base de données, vous pouvez utiliser la méthode sample de la Ruby Array class , qui vous permet de sélectionner un élément aléatoire à partir d'un tableau.

Model.all.sample

cette méthode ne nécessite qu'une requête de base de données, mais elle est significativement plus lente que les alternatives comme Model.offset(rand(Model.count)).first qui besoin de deux requêtes de base de données, si ce dernier est toujours préféré.

10
répondu Ryan Atallah 2014-05-02 23:05:31

Je l'utilise si souvent de la console j'étends ActiveRecord dans un initialiseur-Rails 4 Exemple:

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

je peux Foo.random ramener un enregistrement aléatoire.

8
répondu Knotty66 2013-11-11 13:15:16

Une requête dans Postgres:

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

utilisant un offset, deux requêtes:

offset = rand(User.count) # returns an integer between 0 and (User.count - 1)
Model.offset(offset).limit(1)
5
répondu Thomas Klemm 2016-11-22 20:34:42

lire tout cela ne m'a pas donné beaucoup de confiance sur lequel de ceux-ci fonctionnerait le mieux dans ma situation particulière avec les Rails 5 et MySQL/Maria 5.5. Donc j'ai testé certaines réponses sur ~ 65000 enregistrements, et j'ai deux prises aways:

  1. RAND() avec un limit est un gagnant clair.
  2. ne pas utiliser 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)

cette réponse synthétise, valide et met à jour la réponse de Mohamed , ainsi que le commentaire de Nami WANG sur le même et le commentaire de Florian Pilz sur la réponse acceptée - s'il vous plaît envoyez des votes à eux!

4
répondu Sam 2018-01-02 11:34:12

si vous devez sélectionner quelques résultats aléatoires dans le champ d'application spécifié :

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

rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)
2
répondu Yuri Karpovich 2015-03-04 08:40:15

vous pouvez utiliser la Array méthode sample , la méthode sample renvoie un objet aléatoire à partir d'un tableau, afin de l'utiliser vous avez juste besoin d'exec dans une simple ActiveRecord requête qui renvoie une collection, par exemple:

User.all.sample

retournera quelque chose comme ceci:

#<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">
2
répondu trejo08 2018-04-17 19:41:38

la méthode Ruby pour choisir au hasard un article d'une liste est sample . Voulant créer un sample efficace pour ActiveRecord, et basé sur les réponses précédentes, j'ai utilisé:

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

j'ai mis ceci dans lib/ext/sample.rb et puis le charger avec ceci dans config/initializers/monkey_patches.rb :

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

ce sera une requête si la taille du modèle est déjà mise en cache et deux autres.

1
répondu dankohn 2015-08-30 04:02:21

Rails 4.2 et Oracle :

pour oracle vous pouvez définir une portée sur votre modèle comme ceci:

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

ou

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

et puis pour un échantillon l'appeler comme ceci:

Model.random_order.take(10)

ou

Model.random_order.limit(5)

bien sûr, vous pouvez également placer une commande sans portée comme ceci:

Model.all.order('DBMS_RANDOM.RANDOM') # or DBMS_RANDOM.VALUE respectively
1
répondu mahatmanich 2016-01-07 09:08:23

Pour la base de données MySQL essayer: Modèle.order ("RAND ()").premier

1
répondu Vadim Eremeev 2016-07-05 11:59:32

si vous utilisez PostgreSQL 9.5+, vous pouvez utiliser TABLESAMPLE pour sélectionner un enregistrement aléatoire.

les deux méthodes d'échantillonnage par défaut ( SYSTEM et BERNOULLI ) exigent que vous spécifiez le nombre de lignes à retourner en pourcentage du nombre total de lignes dans le tableau.

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

pour ce faire, il faut connaître le nombre d'enregistrements dans le tableau pour choisir le pourcentage approprié, lequel peut-être pas facile à trouver rapidement. Heureusement, il y a le tsm_system_rows module qui vous permet de spécifier le nombre de lignes à retourner directement.

CREATE EXTENSION tsm_system_rows;

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

pour utiliser ceci dans ActiveRecord, activez d'abord l'extension dans une migration:

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

puis modifier la clause from de la requête:

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

Je ne sais pas si la méthode d'échantillonnage SYSTEM_ROWS sera entièrement aléatoire ou si elle retourne juste la première ligne d'une page aléatoire.

la plupart de ces informations ont été extraites d'un 2ndquadrant blog post écrit par Gulcin Yildirim .

1
répondu Adam Sheehan 2016-11-06 18:40:45

après avoir vu tant de réponses, j'ai décidé de les comparer dans ma base de données PostgreSQL(9.6.3). J'utilise une plus petite table de 100 000 et je me suis débarrassé du modèle.ordre ("RANDOM ()").d'abord parce que c'était déjà deux ordres de grandeur plus lent.

en utilisant un tableau avec 2,500,000 entrées avec 10 colonnes le gagnant hands down était la méthode pluck étant presque 8 fois plus rapide que le runner up(offset. J'ai seulement lancé ceci sur un serveur local de sorte que le nombre pourrait être gonflé mais son plus assez que le plumer méthode est ce que je vais utiliser. Il est également intéressant de noter que cela pourrait causer des problèmes est que vous pluck plus de 1 résultat à la fois, car chacun de ceux-ci sera unique aka moins aléatoire.

Pluck gagne 100 fois sur ma table de 25 000 000 Edit: en fait cette fois inclut le pluck dans la boucle si je le sors il tourne à peu près aussi vite que l'itération simple sur l'id. Cependant, il prend une bonne quantité de 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)

Voici les données de 2000 fois sur ma table de 100 000 lignes pour exclure au hasard

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
répondu Mendoza 2017-09-22 22:32:14

recommande fortement ce gem pour les enregistrements aléatoires, qui est spécialement conçu pour la table avec beaucoup de lignes de données:

https://github.com/haopingfan/quick_random_records

toutes les autres réponses fonctionnent mal avec large base de données, sauf cette gemme:

  1. quick_random_records seul coût 4.6ms totalement.

enter image description here

  1. le User.order('RAND()').limit(10) coût 733.0ms .

enter image description here

  1. les accepté de répondre "151930920 approche du" coût 245.4ms totalement.

enter image description here

  1. le "151950920 approche du" coût 573.4ms .

enter image description here


Note: mon tableau ne compte que 120 000 utilisateurs. Plus vous avez de disques, plus la différence de performance sera énorme.

1
répondu Derek Fan 2018-05-29 08:52:10

je suis tout nouveau à RoR mais j'ai obtenu cela pour travailler pour moi:

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

il vient de:

comment trier au hasard (brouiller) un tableau en rubis?

0
répondu Aaron Pennington 2017-05-23 12:10:54

Qu'à faire:

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

Pour moi, c'est bien clair

0
répondu poramo 2017-10-12 19:47:35

j'essaie ceci de L'exemple de Sam sur mon App en utilisant des rails 4.2.8 de Benchmark( j'ai mis 1..Catégorie.comptez pour aléatoire, parce que si le hasard prend un 0 Il va produire une erreur(ActiveRecord:: RecordNotFound: ne pouvait pas trouver la catégorie avec 'id'=0) et la mine était:

 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
répondu rld 2017-12-29 18:40:16

.order('RANDOM()').limit(limit) semble soigné mais est lent pour les grandes tables parce qu'il a besoin de récupérer et trier toutes les lignes même si limit est 1 (en interne dans la base de données mais pas dans les Rails). Je ne suis pas sûr de MySQL, mais ça arrive à Postgres. Plus d'explications dans ici et ici .

une solution pour les grandes tables est .from("products TABLESAMPLE SYSTEM(0.5)")0.5 signifie 0.5% . Cependant, je trouve que cette solution est encore lente si vous avoir WHERE conditions qui filtrent un grand nombre de lignes. Je suppose que c'est parce que TABLESAMPLE SYSTEM(0.5) récupération de toutes les lignes avant WHERE conditions s'appliquent.

une autre solution pour les grandes tables (mais pas très aléatoire) est:

products_scope.limit(sample_size).sample(limit)

sample_size peut-être 100 (mais pas trop grand sinon c'est lent et consomme beaucoup de mémoire), et limit peut-être 1 . Notez que bien que ce soit rapide mais ce n'est pas vraiment aléatoire, c'est aléatoire dans les dossiers sample_size seulement.

PS: les résultats des benchmarks dans les réponses ci-dessus ne sont pas fiables (au moins dans Postgres) parce que certaines requêtes DB tournant à la 2ème fois peuvent être significativement plus rapides que tournant à la 1ère fois, grâce au cache DB. Et malheureusement, il n'y a pas de moyen facile de désactiver le cache dans Postgres pour rendre ces benchmarks fiables.

0
répondu Linh Dam 2018-04-27 12:20:17