Rails 3: Obtenez un enregistrement aléatoire


132

J'ai donc trouvé plusieurs exemples pour trouver un enregistrement aléatoire dans Rails 2 - la méthode préférée semble être:

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

Étant un débutant, je ne sais pas comment cela pourrait être construit en utilisant la nouvelle syntaxe de recherche dans Rails 3.

Alors, quel est le "Rails 3 Way" pour trouver un enregistrement aléatoire?



9
^^ sauf que je recherche spécifiquement la manière optimale de Rails 3, ce qui est tout le but de la question.
Andrew

rails 3 specific is only query chain :)
fl00r

Réponses:


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

ou

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

En fait, dans Rails 3, tous les exemples fonctionneront. Mais l'utilisation de l'ordre RANDOMest assez lente pour les grandes tables mais plus de style SQL

UPD. Vous pouvez utiliser l'astuce suivante sur une colonne indexée (syntaxe PostgreSQL):

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

11
Cependant, votre premier exemple ne fonctionnera pas avec MySQL - la syntaxe de MySQL est Thing.first (: order => "RAND ()") (un danger d'écrire SQL plutôt que d'utiliser les abstractions ActiveRecord)
DanSingerman

@ DanSingerman, oui c'est spécifique à DB RAND()ou RANDOM(). Merci
fl00r

Et cela ne créera pas de problèmes s'il manque des éléments de l'index? (Si quelque chose au milieu de la pile est supprimé, y aura-t-il une chance qu'il soit demandé?
Victor S

@VictorS, non, #offset va simplement au prochain enregistrement disponible. Je l'ai testé avec Ruby 1.9.2 et Rails 3.1
SooDesuNe

1
@JohnMerlino, oui 0 est offset, pas id. Offet 0 signifie le premier article selon la commande.
fl00r

29

Je travaille sur un projet ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ) où se trouve la base de données localhost et la table des utilisateurs a un peu plus de 100K enregistrements .

En utilisant

ordre par RAND ()

est assez lent

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

devient

SÉLECTIONNER users. * DEusers ORDER BY RAND (id) LIMIT 1

et prend de 8 à 12 secondes pour répondre !!

Journal des rails:

Charge utilisateur (11030.8ms) SELECT users. * FROM usersORDER BY RAND () LIMIT 1

de mysql's expliquer

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

Vous pouvez voir qu'aucun index n'est utilisé ( possible_keys = NULL ), une table temporaire est créée et une passe supplémentaire est requise pour récupérer la valeur souhaitée ( extra = Utilisation temporaire; Utilisation de filesort ).

D'autre part, en divisant la requête en deux parties et en utilisant Ruby, nous avons une amélioration raisonnable du temps de réponse.

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

(; nul pour l'utilisation de la console)

Journal des rails:

Charge utilisateur (25,2 ms) SELECT id FROM usersCharge utilisateur (0,2 ms) SELECT users. * FROM usersWHERE users. id= 106854 LIMITE 1

et mysql's expliquent pourquoi:

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

nous pouvons maintenant utiliser uniquement les index et la clé primaire et faire le travail environ 500 fois plus vite!

METTRE À JOUR:

comme indiqué par icantbecool dans les commentaires, la solution ci-dessus présente un défaut s'il y a des enregistrements supprimés dans le tableau.

Une solution de contournement qui peut être

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

ce qui se traduit par deux requêtes

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

et fonctionne en environ 500 ms.


l'ajout de ".id" après "last" à votre deuxième exemple évitera une erreur "Impossible de trouver le modèle sans ID". Par exemple, User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine

Avertissement! Dans MySQL RAND(id)ne vous donnera PAS un ordre aléatoire différent à chaque requête. Utilisez RAND()si vous voulez un ordre différent à chaque requête.
Justin Tanner

User.find (users.first (Random.rand (users.length)). Last.id) ne fonctionnera pas si un enregistrement a été supprimé. [1,2,4,5,] et il pourrait potentiellement choisir l'id de 3, mais il n'y aurait pas de relation d'enregistrement active.
icantbecool

De plus, users = User.scoped.select (: id); nil n'est pas obsolète. Utilisez ceci à la place: users = User.where (nil) .select (: id)
icantbecool

Je crois que l'utilisation de Random.rand (users.length) comme paramètre pour le premier est un bogue. Random.rand peut renvoyer 0. Lorsque 0 est utilisé comme paramètre en premier, la limite est définie sur zéro et cela ne renvoie aucun enregistrement. Ce qu'il faut utiliser à la place est 1 + Random (users.length) en supposant que users.length> 0.
SWoo

12

Si vous utilisez Postgres

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

Si vous utilisez MySQL

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

Dans les deux cas, vous sélectionnez 5 enregistrements au hasard dans la table Users. Voici la requête SQL réelle affichée dans la console.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

11

J'ai fait une gemme de rails 3 pour faire cela qui fonctionne mieux sur les grandes tables et vous permet d'enchaîner les relations et les portées:

https://github.com/spilliton/randumb

(modifier): Le comportement par défaut de ma gemme utilise essentiellement la même approche que ci-dessus maintenant, mais vous avez la possibilité d'utiliser l'ancienne méthode si vous le souhaitez :)


6

La plupart des réponses publiées ne fonctionnent pas bien sur des tableaux assez volumineux (1 + million de lignes). La commande aléatoire prend rapidement quelques secondes, et faire un compte sur la table prend également assez de temps.

Une solution qui fonctionne bien pour moi dans cette situation est d'utiliser RANDOM()avec une condition where:

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

Sur une table de plus d'un million de lignes, cette requête prend généralement moins de 2 ms.


Un autre avantage de votre solution est d'utiliser une takefonction qui donne une LIMIT(1)requête mais renvoie un élément unique au lieu d'un tableau. Nous n'avons donc pas besoin d'invoquerfirst
Piotr Galas

Il me semble que les enregistrements au début du tableau ont une probabilité plus élevée d'être sélectionnés de cette façon, ce qui n'est peut-être pas ce que vous voulez obtenir.
gorn

5

Et c'est parti

voie de rails

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

usage

Model.random #returns single random object

ou la seconde pensée est

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

usage:

Model.random #returns shuffled collection

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

s'il n'y a pas d'utilisateurs et que vous voulez en obtenir 2, vous obtenez des erreurs. avoir du sens.
Tim Kretschmer

1
La deuxième approche ne fonctionnera pas avec postgres, mais vous pouvez utiliser à la "RANDOM()"place ...
Daniel Richter

4

Cela m'a été très utile mais j'avais besoin d'un peu plus de flexibilité, c'est donc ce que j'ai fait:

Cas 1: Recherche d'une source d' enregistrement aléatoire : site
de trevor turk Ajoutez ceci au modèle 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

alors dans votre contrôleur, vous pouvez appeler quelque chose comme ça

@thing = Thing.random

Cas 2: Recherche de plusieurs enregistrements aléatoires (sans répétition) source:
je ne me souviens pas que j'avais besoin de trouver 10 enregistrements aléatoires sans répétition, voici donc ce que j'ai trouvé fonctionné
dans votre contrôleur:

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

Cela permettra de trouver 10 enregistrements aléatoires, mais il convient de mentionner que si la base de données est particulièrement volumineuse (des millions d'enregistrements), ce ne serait pas idéal et les performances en seraient affectées. Il fonctionnera bien jusqu'à quelques milliers de disques, ce qui me suffisait.


4

La méthode Ruby pour choisir au hasard un élément dans une liste est sample. Voulant créer un efficace samplepour 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 lib/ext/sample.rbet puis le charge avec ceci dans config/initializers/monkey_patches.rb:

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

En fait, #countfera un appel à la base de données pour un fichier COUNT. Si l'enregistrement est déjà chargé, cela peut être une mauvaise idée. Un refactor serait à utiliser à la #sizeplace car il décidera s'il #countdoit être utilisé, ou, si l'enregistrement est déjà chargé, à utiliser #length.
BenMorganIO

Passé de countà en sizefonction de vos commentaires. Plus d'infos sur: dev.mensfeld.pl/2014/09/…
Dan Kohn

3

Fonctionne dans Rails 5 et est indépendant de DB:

Ceci dans votre contrôleur:

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

Vous pouvez, bien sûr, mettre cela dans un souci comme indiqué ici .

app / modèles / préoccupations / 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

puis...

app / models / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Ensuite, vous pouvez utiliser simplement en faisant:

Books.random

ou

Books.random(3)

Cela prend toujours les enregistrements suivants, qui doivent au moins être documentés (car ce n'est peut-être pas ce que l'utilisateur souhaite).
gorn

2

Vous pouvez utiliser sample () dans ActiveRecord

Par exemple

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

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


33
C'est une très mauvaise requête à utiliser si vous avez une grande quantité d'enregistrements, car la base de données sélectionnera TOUS les enregistrements, puis Rails en choisira cinq - un gaspillage énorme.
DaveStephens

5
samplen'est pas dans ActiveRecord, l'échantillon est dans Array. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans

3
C'est un moyen coûteux d'obtenir un enregistrement aléatoire, en particulier à partir d'une grande table. Rails chargera un objet pour chaque enregistrement de votre table en mémoire. Si vous avez besoin d'une preuve, lancez 'rails console', essayez 'SomeModelFromYourApp.find (: all) .sample (5)' et regardez le SQL produit.
Eliot Sykes

1
Voir ma réponse, qui transforme cette réponse coûteuse en une beauté simplifiée pour obtenir plusieurs enregistrements aléatoires.
Arcolye

1

Si vous utilisez Oracle

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

Production

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

1

Je recommande vivement ce joyau pour les enregistrements aléatoires, spécialement conçu 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 grande base de données, à l'exception de ce joyau:

  1. quick_random_records ne coûte que 4.6mstotalement.

entrez la description de l'image ici

  1. le User.order('RAND()').limit(10)coût de la réponse acceptée 733.0ms.

entrez la description de l'image ici

  1. l' offsetapproche a coûté 245.4mstotalement.

entrez la description de l'image ici

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

entrez la description de l'image ici

Remarque: ma table ne compte que 120 000 utilisateurs. Plus vous avez d'enregistrements, plus la différence de performances sera énorme.


METTRE À JOUR:

Performer sur une table de 550000 lignes

  1. Model.where(id: Model.pluck(:id).sample(10)) Coût 1384.0ms

entrez la description de l'image ici

  1. gem: quick_random_recordsne coûte que 6.4mstotalement

entrez la description de l'image ici


-2

Un moyen très simple d'obtenir plusieurs enregistrements aléatoires de la table. Cela fait 2 requêtes bon marché.

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

Vous pouvez changer le "3" pour le nombre d'enregistrements aléatoires que vous voulez.


1
non, la partie Model.pluck (: id) .sample (3) n'est pas bon marché. Il lira le champ id pour chaque élément de la table.
Maximiliano Guzman

Existe-t-il un moyen plus rapide et indépendant de la base de données?
Arcolye

-5

Je viens de rencontrer ce problème en développant une petite application dans laquelle je voulais sélectionner une question aléatoire dans ma base de données. J'ai utilisé:

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

Et ça marche bien pour moi. Je ne peux pas parler des performances des bases de données plus importantes, car il ne s'agit que d'une petite application.


Ouais, il s'agit juste d'obtenir tous vos enregistrements et d'utiliser des méthodes de tableau ruby ​​dessus. L'inconvénient est bien sûr que cela signifie charger tous vos enregistrements en mémoire, puis les réorganiser aléatoirement, puis saisir le deuxième élément dans le tableau réorganisé. Cela pourrait certainement être une perte de mémoire si vous traitez avec un grand ensemble de données. Petit à part, pourquoi ne pas saisir le premier élément? (ie. shuffle[0])
Andrew

doit être aléatoire [0]
Marcelo Austria
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.