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.
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.
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
.
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
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.
ç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.
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)
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é.
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.
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)
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:
- RAND() avec un
limit
est un gagnant clair. - 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!
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)
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">
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.
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
Pour la base de données MySQL essayer: Modèle.order ("RAND ()").premier
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 .
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)
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:
- quick_random_records seul coût
4.6ms
totalement.
- le
User.order('RAND()').limit(10)
coût733.0ms
.
- les accepté de répondre "151930920 approche du" coût
245.4ms
totalement.
- le "151950920 approche du" coût
573.4ms
.
Note: mon tableau ne compte que 120 000 utilisateurs. Plus vous avez de disques, plus la différence de performance sera énorme.
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:
Qu'à faire:
rand_record = Model.find(Model.pluck(:id).sample)
Pour moi, c'est bien clair
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)
.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)")
où 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)
où 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.