Rieles 3: Obtener registro aleatorio

132

Entonces, he encontrado varios ejemplos para encontrar un registro aleatorio en Rails 2; el método preferido parece ser:

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

Al ser un novato, no estoy seguro de cómo podría construirse esto utilizando la nueva sintaxis de búsqueda en Rails 3.

Entonces, ¿cuál es el "Rails 3 Way" para encontrar un registro aleatorio?

Andrés
fuente
9
^^ excepto que estoy buscando específicamente la forma óptima de Rails 3, que es el objetivo de la pregunta.
Andrew
rails 3 específico es solo cadena de consulta :)
fl00r

Respuestas:

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

o

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

En realidad, en Rails 3 todos los ejemplos funcionarán. Pero usar el orden RANDOMes bastante lento para tablas grandes pero más estilo sql

UPD Puede usar el siguiente truco en una columna indexada (sintaxis PostgreSQL):

select * 
from my_table 
where id >= trunc(
  random() * (select max(id) from my_table) + 1
) 
order by id 
limit 1;
fl00r
fuente
11
Sin embargo, su primer ejemplo no funcionará en MySQL: la sintaxis para MySQL es Thing.first (: order => "RAND ()") (un peligro de escribir SQL en lugar de usar las abstracciones ActiveRecord)
DanSingerman
@ DanSingerman, sí, es específico de DB RAND()o RANDOM(). Gracias
fl00r
¿Y esto no creará problemas si faltan elementos en el índice? (si se elimina algo en el medio de la pila, ¿habrá alguna posibilidad de que se solicite?
Victor S
@VictorS, no, no se desplazará solo al siguiente registro disponible. Lo probé con Ruby 1.9.2 y Rails 3.1
SooDesuNe
1
@JohnMerlino, sí 0 es offset, no id. Offet 0 significa primer artículo de acuerdo con el pedido.
fl00r
29

Estoy trabajando en un proyecto ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ) donde el db está en localhost y la tabla de usuarios tiene un poco más de 100K registros .

Utilizando

ordenar por RAND ()

es bastante lento

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

se convierte

SELECCIONAR users. * DE usersORDEN POR RAND (id) LÍMITE 1

y toma de 8 a 12 segundos para responder !!

Registro de rieles:

Carga del usuario (11030.8ms) SELECCIONAR users. * DE usersORDEN POR RAND () LÍMITE 1

de explicar mysql

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

Puede ver que no se utiliza ningún índice ( possible_keys = NULL ), se crea una tabla temporal y se requiere un pase adicional para obtener el valor deseado ( extra = Uso temporal; Uso de clasificación de archivos ).

Por otro lado, al dividir la consulta en dos partes y usar Ruby, tenemos una mejora razonable en el tiempo de respuesta.

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

(; nulo para uso de consola)

Registro de rieles:

User Load (25.2ms) SELECT id FROM usersUser Load (0.2ms) SELECT users. * FROM usersWHERE users. id= 106854 LÍMITE 1

y la explicación de mysql prueba por qué:

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

¡ahora solo podemos usar índices y la clave principal y hacer el trabajo unas 500 veces más rápido!

ACTUALIZAR:

Como lo señala icantbecool en los comentarios, la solución anterior tiene un defecto si hay registros eliminados en la tabla.

Una solución alternativa en eso puede ser

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

que se traduce en dos consultas

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

y se ejecuta en unos 500 ms.

xlembouras
fuente
agregar ".id" después de "último" a su segundo ejemplo evitará el error "No se pudo encontrar el modelo sin ID". Ej. User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine
¡Advertencia! En MySQL RAND(id)será NO le dará un orden aleatorio diferente cada consulta. Úselo RAND()si desea un orden diferente en cada consulta.
Justin Tanner
User.find (users.first (Random.rand (users.length)). Last.id) no funcionará si se eliminó un registro. [1, 2, 4, 5] y potencialmente podría elegir la identificación de 3, pero no habría una relación de registro activa.
icantbecool
Además, users = User.scoped.select (: id); nil no está en desuso. Utilice esto en su lugar: users = User.where (nil) .select (: id)
icantbecool
Creo que usar Random.rand (users.length) como parámetro para primero es un error. Random.rand puede devolver 0. Cuando se usa 0 como parámetro para primero, el límite se establece en cero y esto no devuelve ningún registro. Lo que se debe usar en su lugar es 1 + Aleatorio (users.length) suponiendo users.length> 0.
SWoo
12

Si usa Postgres

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

Si usa MySQL

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

En ambos casos, está seleccionando 5 registros al azar de la tabla Usuarios. Aquí se muestra la consulta SQL real en la consola.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5
icantbecool
fuente
11

Hice una gema de rieles 3 para hacer esto que funciona mejor en tablas grandes y le permite encadenar relaciones y ámbitos:

https://github.com/spilliton/randumb

(editar): el comportamiento predeterminado de mi gema básicamente utiliza el mismo enfoque que el anterior ahora, pero tiene la opción de usar el método antiguo si lo desea :)

spilliton
fuente
6

Muchas de las respuestas publicadas en realidad no funcionarán bien en tablas bastante grandes (1+ millones de filas). La ordenación aleatoria tarda unos segundos y hacer un recuento en la mesa también lleva bastante tiempo.

Una solución que me funciona bien en esta situación es usar RANDOM()una condición where:

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

En una tabla con más de un millón de filas, esta consulta generalmente toma menos de 2 ms.

fivedigit
fuente
Otra ventaja de su solución es la takefunción de uso que proporciona LIMIT(1)consultas pero devuelve un solo elemento en lugar de una matriz. Así que no necesitamos invocarlofirst
Piotr Galas
Me parece que los registros al comienzo de la tabla tienen una mayor probabilidad de ser seleccionados de esta manera, lo que podría no ser lo que desea lograr.
gorn
5

aquí vamos

rieles camino

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

uso

Model.random #returns single random object

o el segundo pensamiento es

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

uso:

Model.random #returns shuffled collection
Tim Kretschmer
fuente
Couldn't find all Users with 'id': (first, {:offset=>1}) (found 0 results, but was looking for 2)
Bruno
Si no hay ningún usuario y quieres obtener 2, entonces obtienes errores. tener sentido.
Tim Kretschmer el
1
El segundo enfoque no funcionará con postgres, pero puedes usarlo "RANDOM()"en su lugar ...
Daniel Richter
4

Esto fue muy útil para mí, sin embargo, necesitaba un poco más de flexibilidad, así que esto es lo que hice:

Caso 1: Encontrar una fuente de registro aleatorio : sitio trevor turk
Agregue esto al modelo 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

entonces en tu controlador puedes llamar a algo como esto

@thing = Thing.random

Caso 2: Encontrar múltiples registros aleatorios (sin repeticiones) fuente: no puedo recordar
que necesitaba encontrar 10 registros aleatorios sin repeticiones, así que esto es lo que encontré funcionado
en su controlador:

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

Esto encontrará 10 registros aleatorios, sin embargo, vale la pena mencionar que si la base de datos es particularmente grande (millones de registros), esto no sería ideal y el rendimiento se verá obstaculizado. Se realizará bien hasta unos pocos miles de registros, lo cual fue suficiente para mí.

Hishalv
fuente
4

El método Ruby para elegir aleatoriamente un elemento de una lista es sample. Queriendo crear un eficiente samplepara ActiveRecord, y en base a las respuestas anteriores, utilicé:

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

Puse esto lib/ext/sample.rby luego lo cargué con esto en config/initializers/monkey_patches.rb:

Dir[Rails.root.join('lib/ext/*.rb')].each { |file| require file }
Dan Kohn
fuente
En realidad, #counthará una llamada a la base de datos para a COUNT. Si el registro ya está cargado, esta podría ser una mala idea. Un refactor sería usar #sizeen su lugar, ya que decidirá si #countdebe usarse o, si el registro ya está cargado, usar #length.
BenMorganIO
Cambió de counta sizebasado en sus comentarios. Más información en: dev.mensfeld.pl/2014/09/…
Dan Kohn
3

Funciona en Rails 5 y es independiente de DB:

Esto en su controlador:

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

Por supuesto, puede poner esto en una preocupación como se muestra aquí .

aplicación / modelos / preocupaciones / 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

luego...

app / models / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Entonces puede usar simplemente haciendo:

Books.random

o

Books.random(3)
richardun
fuente
Esto siempre toma registros posteriores, que deben estar al menos documentados (ya que podría no ser lo que el usuario quiere).
Gorn
2

Puede usar sample () en ActiveRecord

P.ej

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

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

Trond
fuente
33
Esta es una consulta muy mala para usar si tiene una gran cantidad de registros, ya que la base de datos seleccionará TODOS los registros, luego Rails elegirá cinco registros de eso, un desperdicio masivo.
DaveStephens
55
sampleno está en ActiveRecord, la muestra está en matriz. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans
3
Esta es una forma costosa de obtener un registro aleatorio, especialmente de una tabla grande. Rails cargará un objeto para cada registro de su tabla en la memoria. Si necesita pruebas, ejecute 'rails console', pruebe 'SomeModelFromYourApp.find (: all) .sample (5)' y observe el SQL producido.
Eliot Sykes
1
Vea mi respuesta, que convierte esta costosa respuesta en una belleza optimizada para obtener múltiples registros aleatorios.
Arcolye
1

Si usa Oracle

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

Salida

SELECT * FROM users ORDER BY DBMS_RANDOM.VALUE WHERE ROWNUM <= 10
Marcelo Austria
fuente
1

Recomiendo esta gema para registros aleatorios, que está especialmente diseñada para tablas con muchas filas de datos:

https://github.com/haopingfan/quick_random_records

Todas las demás respuestas funcionan mal con una base de datos grande, excepto esta gema:

  1. quick_random_records solo cuesta 4.6mstotalmente.

ingrese la descripción de la imagen aquí

  1. El User.order('RAND()').limit(10)costo de la respuesta aceptada 733.0ms.

ingrese la descripción de la imagen aquí

  1. El offsetenfoque costó 245.4mstotalmente.

ingrese la descripción de la imagen aquí

  1. El User.all.sample(10)costo de aproximación 573.4ms.

ingrese la descripción de la imagen aquí

Nota: Mi mesa solo tiene 120,000 usuarios. Cuantos más registros tenga, más enorme será la diferencia de rendimiento.


ACTUALIZAR:

Realizar en la mesa con 550,000 filas

  1. Model.where(id: Model.pluck(:id).sample(10)) costo 1384.0ms

ingrese la descripción de la imagen aquí

  1. gem: quick_random_recordssolo cuesta 6.4mstotalmente

ingrese la descripción de la imagen aquí

Derek Fan
fuente
-2

Una forma muy fácil de obtener múltiples registros aleatorios de la tabla. Esto hace 2 consultas baratas.

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

Puede cambiar el "3" por la cantidad de registros aleatorios que desee.

Arcolye
fuente
1
no, la parte Model.pluck (: id) .sample (3) no es barata. Leerá el campo de identificación para cada elemento de la tabla.
Maximiliano Guzman
¿Hay una forma más rápida de agnóstico de base de datos?
Arcolye
-5

Me encontré con este problema desarrollando una pequeña aplicación en la que quería seleccionar una pregunta aleatoria de mi base de datos. Solía:

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

Y me está funcionando bien. No puedo hablar sobre el rendimiento de las bases de datos más grandes, ya que esta es solo una pequeña aplicación.

rails_newbie
fuente
Sí, esto es solo obtener todos tus registros y usar métodos de matriz de rubíes en ellos. El inconveniente es que, por supuesto, significa cargar todos sus registros en la memoria, luego reordenarlos al azar, y luego tomar el segundo elemento en la matriz reordenada. Definitivamente, podría ser un problema de memoria si se trata de un gran conjunto de datos. Menor aparte, ¿por qué no agarrar el primer elemento? (es decir shuffle[0])
Andrew
debe ser barajado [0]
Marcelo Austria