web-dev-qa-db-fra.com

Enregistrement aléatoire dans ActiveRecord

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

Cependant, j'ai également rencontré une autre solution via une recherche Google (impossible d'attribuer un lien en raison de nouvelles restrictions d'utilisateurs):

 Rand_id = Rand(Model.count)
 Rand_record = Model.first(:conditions => ["id >= ?", Rand_id])

Je suis curieux de savoir comment les autres l'ont fait ou si quelqu'un sait de quelle manière il serait plus efficace.

140
jyunderwood

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é aléatoirement (jusqu’au nombre d’enregistrements en cours) en tant que 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, je viens d'utiliser ORDER BY Rand () ou RANDOM () (selon la base de données). Ce n'est pas un problème de performance si vous n'avez pas de problème de performance.

127
Toby Hede

Dans Rails 4 et 5, utilisez Postgresql ou SQLite, à l'aide de RANDOM():

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

Probablement la même chose fonctionnerait pour MySQL avec Rand()

Model.order("Rand()").first

Ceci environ 2,5 fois plus rapide que l’approche de la réponse acceptée .

Caveat: cette procédure est lente pour les jeux de données volumineux contenant des millions d'enregistrements. Vous pouvez donc ajouter une clause limit.

168
Mohamad

Votre exemple de code commencera à se comporter de manière incorrecte une fois les enregistrements supprimés (il favorisera injustement les éléments avec des identifiants plus faibles).

Vous feriez probablement mieux d'utiliser les méthodes aléatoires de votre base de données. Celles-ci varient selon la base de données utilisée, mais: order => "Rand ()" fonctionne pour mysql et: order => "RANDOM ()" fonctionne pour postgres.

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

Analyse comparative de ces deux méthodes sur MySQL 5.1.49, Ruby 1.9.2p180 sur une table product 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)

Le décalage dans MySQL semble être beaucoup plus lent.

EDIT J'ai aussi essayé

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

Mais je devais le tuer après environ 60 secondes. MySQL était "Copier dans une table tmp sur le disque". Cela ne va pas au travail. 

27
dkam

Cela n'a pas à être si difficile.

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

pluck renvoie un tableau de tous les identifiants de la table. La méthode sample du tableau renvoie un identifiant aléatoire du tableau.

Cela devrait fonctionner correctement, avec une probabilité égale de sélection et de prise en charge pour les tables avec des lignes supprimées. Vous pouvez même le mélanger avec des contraintes.

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

Et choisissez ainsi un utilisateur aléatoire qui aime les vendredis plutôt que n'importe quel utilisateur.

18
Niels B.

J'ai fabriqué un joyau Rails 3 pour gérer ceci:

https://github.com/spilliton/randumb

Cela vous permet de faire des choses comme ceci:

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

Il n’est pas conseillé d’utiliser cette solution, mais si, pour une raison quelconque, vous vraiment souhaitez sélectionner un enregistrement de manière aléatoire tout en effectuant une requête de base de données, vous pouvez utiliser la méthode sample de la classe classe Ruby Array , qui vous permet de sélectionner un élément aléatoire dans un tableau.

Model.all.sample

Cette méthode nécessite uniquement une requête de base de données, mais elle est nettement plus lente que les alternatives telles que Model.offset(Rand(Model.count)).first qui nécessitent deux requêtes de base de données, bien que cette dernière soit toujours préférée.

10
Ryan Atallah

Je l’utilise si souvent depuis la console que j’étends ActiveRecord dans un initializer - exemple avec Rails 4:

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

Je peux alors appeler Foo.random pour ramener un enregistrement aléatoire.

8
Knotty66

La lecture de tous ces éléments ne me permettait pas vraiment de savoir lequel de ceux-ci fonctionnerait le mieux dans ma situation particulière avec Rails 5 et MySQL/Maria 5.5. J'ai donc testé certaines des réponses sur environ 65 000 enregistrements et en ai deux à prendre:

  1. Rand () avec un limit est un gagnant clair.
  2. N'utilisez pas 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 sujet et le commentaire de Florian Pilz sur la réponse acceptée - merci de leur envoyer des votes!

5
Sam

Une requête dans Postgres:

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

En 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
Thomas Klemm

Vous pouvez utiliser la méthode Arraysample, la méthode sample renvoie un objet aléatoire à partir d'un tableau. Pour l'utiliser, il vous suffit d'exécuter une requête ActiveRecord simple qui renvoie une collection, par exemple:

User.all.sample

retournera quelque chose comme ceci:

#<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">
2
trejo08

Si vous devez sélectionner certains résultats aléatoires dans la portée spécifiée :

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
Yuri Karpovich

Après avoir vu tant de réponses, j'ai décidé de les analyser toutes dans ma base de données PostgreSQL (9.6.3). J'utilise une table plus petite de 100 000 et je me suis d'abord débarrassé de Model.order ("RANDOM ()"), car il était déjà deux ordres de grandeur plus lent.

En utilisant une table avec 2.500.000 entrées avec 10 colonnes, le vainqueur n'a gagné que maintes fois, la méthode de pluck étant presque 8 fois plus rapide que le second (offset). Je ne l'ai exécutée que sur un serveur local afin que ce nombre puisse être gonflé Il est également intéressant de noter que cela pourrait causer des problèmes si vous cueillez plus d’un résultat à la fois, car chacun de ceux-ci sera unique, autrement dit moins aléatoire.

Pluck gagne à courir 100 fois sur ma table de 25 000 000 lignes Edit: en fait, cette fois-ci inclut le pluck dans la boucle. Si je le retire, il s'exécute aussi rapidement qu'une simple itération sur l'identifiant. Toutefois; cela prend beaucoup 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 exécutées 2 000 fois sur ma table de 100 000 lignes pour éliminer toute erreur aléatoire.

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
Mendoza

Recommandez vivement cette gemme pour les enregistrements aléatoires, spécialement conçue pour les tables avec beaucoup de lignes de données:

https://github.com/haopingfan/quick_random_records

Toutes les autres réponses fonctionnent mal avec une base de données volumineuse, à l'exception de cette gemme: 

  1. quick_random_records n'a coûté que 4.6ms totalement.

 enter image description here

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

 enter image description here

  1. la réponse acceptée offset approche coût 245.4ms totalement.

 enter image description here

  1. l'approche User.all.sample(10) coût 573.4ms.

 enter image description here


Remarque: Ma table ne compte que 120 000 utilisateurs. Plus vous avez de disques, plus la différence de performance sera énorme.

1
Derek Fan

Pour la base de données MySQL, essayez: Model.order ("Rand ()"). First

1
Vadim Eremeev

La méthode Ruby pour sélectionner au hasard un élément dans une liste est sample. Voulant créer une sample efficace pour ActiveRecord, et sur la base des réponses précédentes, j’ai utilisé:

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

Je mets ceci dans lib/ext/sample.rb et le charge ensuite 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 sinon.

1
dankohn

Rails 4.2 et Oracle :

Pour Oracle, vous pouvez définir une étendue sur votre modèle comme suit:

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

ou 

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

Et puis, pour un échantillon, appelez comme ceci:

Model.random_order.take(10)

ou

Model.random_order.limit(5)

bien sûr, vous pouvez également passer une commande sans une portée comme celle-ci:

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

Si vous utilisez PostgreSQL 9.5+, vous pouvez tirer parti de TABLESAMPLE pour sélectionner un enregistrement aléatoire.

Les deux méthodes d'échantillonnage par défaut (SYSTEM et BERNOULLI) nécessitent que vous spécifiiez le nombre de lignes à renvoyer sous forme de pourcentage du nombre total de lignes de la table.

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

Cela nécessite de connaître la quantité d'enregistrements dans la table pour sélectionner le pourcentage approprié, ce qui peut ne pas être facile à trouver rapidement. Heureusement, il existe le tsm_system_rows module qui vous permet de spécifier le nombre de lignes à renvoyer directement.

CREATE EXTENSION tsm_system_rows;

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

Pour utiliser cela dans ActiveRecord, commencez par activer l'extension dans une migration:

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

Puis modifiez 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 renvoie simplement la première ligne d'une page aléatoire.

La plupart de ces informations sont extraites d'un blog de 2ndQuadrant écrit par Gulcin Yildirim .

1
Adam Sheehan

J'essaie ceci de l'exemple de Sam sur mon application utilisant Rails 4.2.8 of Benchmark (je mets 1..Category.count pour random, car si le random prend 0, il produira une erreur (ActiveRecord :: RecordNotFound: impossible de trouver 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
rld

En plus d'utiliser RANDOM(), vous pouvez également insérer ceci dans une portée:

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

Ou, si vous n'aimez pas cela comme une portée, jetez-le simplement dans une méthode de classe. Maintenant, Thing.random fonctionne avec Thing.random(n).

0
Damien Roche

Qu'en est-il de faire:

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

Pour moi c'est très clair

0
poramo

Très vieille question mais avec:

Rand_record = Model.all.shuffle

Vous avez un tableau d'enregistrement, triez-le selon un ordre aléatoire ... Pas besoin de gemmes ni de scripts.

Si vous voulez un enregistrement:

Rand_record = Model.all.shuffle.first
0
Gregdebrick

Je suis nouveau chez RoR, mais cela a fonctionné pour moi:

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

C'est venu de:

Comment trier au hasard (brouiller) un tableau dans Ruby?

0
Aaron Pennington

.order('RANDOM()').limit(limit) a l'air soigné mais est lent pour les grandes tables car il doit extraire et trier toutes les lignes même si limit est à 1 (en interne dans la base de données mais pas dans Rails). Je ne suis pas sûr de MySQL, mais cela se produit dans 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 cette solution toujours lente si vous avez des conditions WHERE qui filtrent beaucoup de lignes. Je suppose que c’est parce que TABLESAMPLE SYSTEM(0.5) récupère toutes les lignes avant que les conditions WHERE ne s’appliquent.

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

products_scope.limit(sample_size).sample(limit)

sample_size peut être 100 (mais pas trop gros sinon il est lent et consomme beaucoup de mémoire), et limit peut être 1. Notez que bien que cela soit rapide mais que ce ne soit pas vraiment aléatoire, il est aléatoire dans les enregistrements sample_size uniquement.

PS: Les résultats de référence des réponses ci-dessus ne sont pas fiables (du moins dans Postgres), car certaines requêtes de base de données exécutées à la deuxième fois peuvent être beaucoup plus rapides que celles exécutées à la première fois, grâce au cache de base de données. Et malheureusement, il n’existe pas de moyen facile de désactiver le cache dans Postgres pour rendre ces tests fiables. 

0
Linh Dam