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.
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.
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
.
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
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.
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.
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)
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.
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.
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:
limit
est un gagnant clair.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!
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)
Vous pouvez utiliser la méthode Array
sample
, 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">
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)
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)
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:
4.6ms
totalement.User.order('Rand()').limit(10)
coût 733.0ms
.offset
approche coût 245.4ms
totalement.User.all.sample(10)
coût 573.4ms
.Remarque: Ma table ne compte que 120 000 utilisateurs. Plus vous avez de disques, plus la différence de performance sera énorme.
Pour la base de données MySQL, essayez: Model.order ("Rand ()"). First
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.
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
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 .
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)
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)
.
Qu'en est-il de faire:
Rand_record = Model.find(Model.pluck(:id).sample)
Pour moi c'est très clair
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
Je suis nouveau chez RoR, mais cela a fonctionné pour moi:
def random
@cards = Card.all.sort_by { Rand }
end
C'est venu de:
.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)")
où 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)
où 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.