Registro aleatorio en ActiveRecord


151

Necesito obtener un registro aleatorio de una tabla a través de ActiveRecord. He seguido el ejemplo de Jamis Buck de 2006 .

Sin embargo, también he encontrado otra forma a través de una búsqueda en Google (no se puede atribuir con un enlace debido a nuevas restricciones de usuario):

 rand_id = rand(Model.count)
 rand_record = Model.first(:conditions => ["id >= ?", rand_id])

Tengo curiosidad por saber cómo lo han hecho otros aquí o si alguien sabe de qué manera sería más eficiente.


2
2 puntos que pueden ayudar a una respuesta. 1. ¿Qué tan uniformemente distribuidos están sus identificadores, son secuenciales? 2. ¿Qué tan aleatorio debe ser? ¿Bastante bueno al azar, o al azar real?
Michael

Son identificadores secuenciales generados automáticamente por activerecord y solo tiene que ser lo suficientemente bueno.
jyunderwood

1
Entonces su solución propuesta es casi ideal :) Usaría "SELECT MAX (id) FROM table_name" en lugar de COUNT (*) ya que tratará con las filas eliminadas un poco mejor, de lo contrario, el resto está bien. En resumen, si "lo suficientemente bueno" está bien, entonces solo tiene que tener un método que asuma una distribución cercana a la que realmente tiene. Si es uniforme e incluso como has dicho, el rand simple funciona muy bien.
Michael

1
Esto no funcionará cuando haya eliminado filas.
Venkat D.

Respuestas:


136

No he encontrado una manera ideal de hacer esto sin al menos dos consultas.

Lo siguiente utiliza un número generado aleatoriamente (hasta el recuento de registros actual) como compensación .

offset = rand(Model.count)

# Rails 4
rand_record = Model.offset(offset).first

# Rails 3
rand_record = Model.first(:offset => offset)

Para ser honesto, acabo de usar ORDER BY RAND () o RANDOM () (dependiendo de la base de datos). No es un problema de rendimiento si no tiene un problema de rendimiento.


2
El código Model.find(:offset => offset).firstarrojará un error. Creo que Model.first(:offset => offset)podría funcionar mejor.
Harish Shetty

1
Sí, he estado trabajando con Rails 3 y sigo confundiéndome acerca de los formatos de consulta entre versiones.
Toby Hede

77
Tenga en cuenta que el uso de desplazamiento es muy lento con un gran conjunto de datos, ya que en realidad necesita exploración de índice (o exploración de tabla, en caso de que el índice agrupado se use como InnoDB). En otras palabras, es una operación O (N) pero "WHERE id> = # {rand_id} ORDER BY id ASC LIMIT 1" es O (log N), que es mucho más rápido.
Kenn

15
Tenga en cuenta que el enfoque de compensación solo produce un único punto de datos encontrado aleatoriamente (el primero, todos después todavía se ordenan por id). Si necesita múltiples registros seleccionados al azar, debe usar este enfoque varias veces o usar el método de orden aleatorio proporcionado por su base de datos, es decir, Thing.order("RANDOM()").limit(100)para 100 entradas seleccionadas al azar. (Tenga en cuenta que está RANDOM()en PostgreSQL y RAND()en MySQL ... no es tan portátil como desee).
Florian Pilz

3
No funciona para mí en Rails 4. Uso Model.offset(offset).first.
mahemoff

206

Carriles 6

Como dijo Jason en los comentarios, en Rails 6, los argumentos sin atributos no están permitidos. Debe ajustar el valor en una Arel.sql()declaración.

Model.order(Arel.sql('RANDOM()')).first

Carriles 5, 4

En los rieles 4 y 5 , usando Postgresql o SQLite , usando RANDOM():

Model.order('RANDOM()').first

Presumiblemente lo mismo funcionaría para MySQL conRAND()

Model.order('RAND()').first

Esto es aproximadamente 2.5 veces más rápido que el enfoque en la respuesta aceptada .

Advertencia : esto es lento para grandes conjuntos de datos con millones de registros, por lo que es posible que desee agregar una limitcláusula.


44
"Random ()" también funciona en sqlite, por lo que para aquellos de nosotros que todavía desarrollamos en sqlite y ejecutamos postgres en producción, su solución funciona en ambos entornos.
wuliwong

55
Creé un punto de referencia para esto contra la respuesta aceptada. En Postgresql 9.4, el enfoque de esta respuesta es aproximadamente el doble de rápido.
panmari

3
Parece que no se recomienda en mysql webtrenches.com/post.cfm/avoid-rand-in-mysql
Prakash Murthy

Esta es la solución más rápida
Sergio Belevskij

1
"Los argumentos que no son de atributo se rechazarán en Rails 6.0. Este método no debe llamarse con valores proporcionados por el usuario, como parámetros de solicitud o atributos de modelo. Los valores seguros conocidos se pueden pasar envolviéndolos en Arel.sql ()".
Trenton Tyler

73

Su código de ejemplo comenzará a comportarse incorrectamente una vez que se eliminen los registros (favorecerá injustamente los elementos con identificadores inferiores)

Probablemente sea mejor usar los métodos aleatorios dentro de su base de datos. Estos varían según la base de datos que esté utilizando, pero: order => "RAND ()" funciona para mysql y: order => "RANDOM ()" funciona para postgres

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

77
ORDER BY RAND () para MySQL termina en un tiempo de ejecución horrible a medida que aumentan los datos. Es imposible de mantener (dependiendo de los requisitos de tiempo) incluso comenzando en solo miles de filas.
Michael

Michael plantea un gran punto (eso también es cierto para otros DB). Generalmente, seleccionar filas aleatorias de tablas grandes no es algo que desee hacer en una acción dinámica. El almacenamiento en caché es tu amigo. Repensar lo que estás tratando de lograr puede que tampoco sea una mala idea.
semanticart

1
Ordenar RAND () en mysql en una tabla con aproximadamente un millón de filas es slooooooooooooooooooooow.
Subimagen

24
Ya no funciona Usar en su Model.order("RANDOM()").firstlugar.
phil pirozhkov

Lento y específico de la base de datos. Se supone que ActiveRecord funciona a la perfección entre bases de datos, por lo que no debe usar este método.
Dex

29

Comparando estos dos métodos en MySQL 5.1.49, Ruby 1.9.2p180 en una tabla de productos con + 5 millones de registros:

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)

La compensación en MySQL parece ser mucho más lenta.

EDITAR También probé

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

Pero tuve que matarlo después de ~ 60 segundos. MySQL fue "Copiar a la tabla tmp en el disco". Eso no va a funcionar.


1
Para aquellos que buscan más pruebas, cuánto tiempo lleva un enfoque aleatorio real: probé Thing.order("RANDOM()").firsten una tabla con 250,000 entradas; la consulta terminó en menos de medio segundo. (PostgreSQL 9.0, REE 1.8.7, 2 x 2.66 GHz núcleos) Eso es lo suficientemente rápido para mí, ya que estoy haciendo una "limpieza" única.
Florian Pilz

66
El método rand de Ruby devuelve uno menos que el número especificado, por lo que querrá rand_id = rand(Product.count) + 1o nunca obtendrá el último registro.
Ritchie

44
Nota random1no funcionará si alguna vez elimina una fila en la tabla. (El recuento será inferior al ID máximo y nunca podrá seleccionar filas con ID altos).
Nicholas

El uso random2se puede mejorar #orderusando una columna indexada.
Carson Reinke

18

No tiene que ser tan difícil.

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

pluckdevuelve una matriz de todos los id en la tabla. lossample método en la matriz devuelve una identificación aleatoria de la matriz.

Esto debería funcionar bien, con la misma probabilidad de selección y soporte para tablas con filas eliminadas. Incluso puedes mezclarlo con restricciones.

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

Y, por lo tanto, elija un usuario aleatorio al que le gusten los viernes en lugar de cualquier usuario.


8
Esto es limpio y funciona para una mesa pequeña o para un uso único, solo tenga en cuenta que no escalará. En una mesa de 3M, extraer IDs me lleva unos 15 segundos en MariaDB.
mahemoff

2
Ese es un buen punto. ¿Ha encontrado una solución alternativa que sea más rápida y mantenga las mismas cualidades?
Niels B.

¿La solución de compensación aceptada no mantiene las mismas cualidades?
mahemoff

No, no admite condiciones y no tiene la misma probabilidad de selección para tablas con registros eliminados.
Niels B.

1
Ahora que lo pienso, si aplica las restricciones al contar y seleccionar con un desplazamiento, la técnica debería funcionar. Me imaginaba solo aplicándolo en el conteo.
Niels B.

15

No se recomienda que use esta solución, pero si por alguna razón realmente desea seleccionar un registro al azar mientras realiza una sola consulta de base de datos, puede usar el samplemétodo de la clase Ruby Array , que le permite seleccionar un elemento aleatorio de una matriz.

Model.all.sample

Este método requiere solo una consulta a la base de datos, pero es significativamente más lento que las alternativas como las Model.offset(rand(Model.count)).firstque requieren dos consultas a la base de datos, aunque esta última sigue siendo preferida.


99
No hagas esto. Nunca.
Zabba

55
Si tiene 100k filas en su base de datos, todas estas deberían cargarse en la memoria.
Venkat D.

3
Por supuesto, no se recomienda para la producción de código en tiempo real, pero me gusta esta solución, es muy clara para situaciones especiales como la siembra de la base de datos con valores falsos.
fguillen

13
Por favor, nunca digas nunca. Esta es una gran solución para la depuración en tiempo de desarrollo si la tabla es pequeña. (Y si está tomando muestras, la depuración es posiblemente el caso de uso).
mahemoff

Lo estoy usando para sembrar y es bueno para mí. Además, Model.all.sample (n) también funciona :)
Arnaldo Ignacio Gaspar Véjar

13

Hice una gema de rieles 3 para manejar esto:

https://github.com/spilliton/randumb

Te permite hacer cosas como esta:

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

77
En la documentación de esta gema explican que "randumb simplemente agrega un adicional ORDER BY RANDOM()(o RAND()para mysql) a su consulta". - por lo tanto, los comentarios sobre el mal desempeño mencionados en los comentarios a la respuesta de @semanticart también se aplican al usar esta gema. Pero al menos es DB independiente.
Nicolas

8

Lo uso tan a menudo desde la consola que extiendo ActiveRecord en un inicializador - Ejemplo de Rails 4:

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

Entonces puedo llamar Foo.randompara recuperar un registro aleatorio.


1
Qué se necesita limit(1)? ActiveRecord#firstdebería ser lo suficientemente inteligente como para hacer eso.
tokland

6

Una consulta en Postgres:

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

Usando un desplazamiento, dos consultas:

offset = rand(User.count) # returns an integer between 0 and (User.count - 1)
Model.offset(offset).limit(1)

1
No es necesario -1, rand cuenta hasta num - 1
anemaria20

Gracias, cambiado: +1:
Thomas Klemm

5

Leer todo esto no me dio mucha confianza sobre cuál funcionaría mejor en mi situación particular con Rails 5 y MySQL / Maria 5.5. Así que probé algunas de las respuestas en ~ 65000 registros, y tengo dos conclusiones:

  1. RAND () con a limites un claro ganador.
  2. No use 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)

Esta respuesta sintetiza, valida y actualiza la respuesta de Mohamed , así como el comentario de Nami WANG sobre la misma y el comentario de Florian Pilz sobre la respuesta aceptada. ¡Por favor envíeles votos!


3

Puede usar el Arraymétodo sample, el método sampledevuelve un objeto aleatorio de una matriz, para usarlo solo necesita ejecutar en una ActiveRecordconsulta simple que devuelva una colección, por ejemplo:

User.all.sample

devolverá algo como esto:

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

No recomendaría trabajar con métodos de matriz mientras utilizo AR. Esta forma toma casi 8 veces el tiempo que order('rand()').limit(1)hace "el mismo" trabajo (con ~ 10K registros).
Sebastian Palma

3

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 733.0ms.

ingrese la descripción de la imagen aquí

  1. El offsetenfoque de respuesta aceptado cuesta 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.


2

Si necesita seleccionar algunos resultados aleatorios dentro del alcance especificado :

scope :male_names, -> { where(sex: 'm') }
number_of_results = 10

rand = Names.male_names.pluck(:id).sample(number_of_results)
Names.where(id: rand)

1

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, usé:

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 }

Esta será una consulta si el tamaño del modelo ya está en caché y dos en caso contrario.


1

Rails 4.2 y Oracle :

Para Oracle, puede establecer un alcance en su modelo de la siguiente manera:

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

o

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

Y luego, para una muestra, llámalo así:

Model.random_order.take(10)

o

Model.random_order.limit(5)

Por supuesto, también puede realizar un pedido sin un alcance como este:

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

También puedes hacer esto con postgres con order('random()'y MySQL con order('rand()'). Esta es definitivamente la mejor respuesta.
jrochkind

1

Para la base de datos MySQL, pruebe: Model.order ("RAND ()"). First


Esto no funciona en mysql ... deberías incluir al menos con qué motor DB se supone que funciona
Arnold Roa

Lo siento, hubo un error tipográfico. Corregido ahora. Debería funcionar para mysql (solo)
Vadim Eremeev

1

Si está utilizando PostgreSQL 9.5+, puede aprovechar TABLESAMPLE para seleccionar un registro aleatorio.

Los dos métodos de muestreo predeterminados ( SYSTEMy BERNOULLI) requieren que especifique el número de filas a devolver como un porcentaje del número total de filas en la tabla.

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

Esto requiere conocer la cantidad de registros en la tabla para seleccionar el porcentaje apropiado, que puede no ser fácil de encontrar rápidamente. Afortunadamente, existe el tsm_system_rowsmódulo que le permite especificar el número de filas para devolver directamente.

CREATE EXTENSION tsm_system_rows;

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

Para usar esto dentro de ActiveRecord, primero habilite la extensión dentro de una migración:

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

Luego modifique la fromcláusula de la consulta:

customer = Customer.from("customers TABLESAMPLE SYSTEM_ROWS(1)").first

No se si el SYSTEM_ROWS método de muestreo será completamente aleatorio o si solo devuelve la primera fila de una página aleatoria.

La mayor parte de esta información fue tomada de una publicación de blog de 2ndQuadrant escrita por Gulcin Yildirim .


1

Después de ver tantas respuestas, decidí compararlas todas en mi base de datos PostgreSQL (9.6.3). Utilicé una tabla de 100.000 más pequeña y me deshice del Model.order ("RANDOM ()"). Primero, ya que era dos órdenes de magnitud más lento.

Usando una tabla con 2,500,000 entradas con 10 columnas, el ganador fue que el método de selección fue casi 8 veces más rápido que el finalista (desplazamiento. Solo ejecuté esto en un servidor local para que ese número pueda estar inflado, pero es lo suficientemente grande como para que el complemento es lo que terminaré usando. También vale la pena señalar que esto podría causar problemas si seleccionas más de 1 resultado a la vez, ya que cada uno de ellos será único o menos aleatorio.

Pluck gana corriendo 100 veces en mi tabla de 25,000,000 filas Editar: en realidad esta vez incluye el pluck en el bucle si lo elimino, funciona casi tan rápido como una simple iteración en la identificación. Sin embargo; ocupa una buena cantidad 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)

Aquí están los datos que se ejecutan 2000 veces en mi tabla de 100,000 filas para descartar al azar

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

Pregunta muy antigua pero con:

rand_record = Model.all.shuffle

Tienes una matriz de registro, ordenada por orden aleatorio. No necesita gemas ni scripts.

Si quieres un registro:

rand_record = Model.all.shuffle.first

1
No es la mejor opción, ya que esto carga todos los registros en la memoria. Además, shuffle.first==.sample
Andrew Rozhenko el

0

Soy nuevo en RoR pero conseguí que esto funcione para mí:

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

Vino de:

¿Cómo ordenar aleatoriamente (codificar) una matriz en Ruby?


44
Lo malo de esto es que cargará todas las tarjetas de la base de datos. Es más eficiente hacerlo dentro de la base de datos.
Anton Kuzmin el

También puede barajar matrices con array.shuffle. De todos modos, tenga cuidado, ya Card.allque cargará todos los registros de la tarjeta en la memoria, lo que se vuelve más ineficiente a medida que más objetos estamos hablando.
Thomas Klemm

0

Qué hay que hacer:

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

Para mi esta muy claro


0

Intento esto del ejemplo de Sam en mi aplicación usando rails 4.2.8 de Benchmark (pongo 1..Category.count para random, porque si el random toma un 0 producirá un error (ActiveRecord :: RecordNotFound: No se pudo encontrar Categoría con 'id' = 0)) y la mina fue:

 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

.order('RANDOM()').limit(limit)se ve ordenado pero es lento para tablas grandes porque necesita buscar y ordenar todas las filas incluso si limites 1 (internamente en la base de datos pero no en Rails). No estoy seguro acerca de MySQL pero esto sucede en Postgres. Más explicaciones aquí y aquí .

Una solución para tablas grandes es .from("products TABLESAMPLE SYSTEM(0.5)")donde 0.5significa 0.5%. Sin embargo, creo que esta solución sigue siendo lenta si tiene WHEREcondiciones que filtran muchas filas. Supongo que es porque TABLESAMPLE SYSTEM(0.5)buscar todas las filas antesWHERE que se apliquen las condiciones.

Otra solución para tablas grandes (pero no muy aleatorias) es:

products_scope.limit(sample_size).sample(limit)

dónde sample_sizepuede estar 100(pero no demasiado grande, de lo contrario es lento y consume mucha memoria), y limitpuede ser 1. Tenga en cuenta que aunque esto es rápido pero no es realmente aleatorio, es aleatorio sample_sizesolo dentro de los registros.

PD: Los resultados de referencia en las respuestas anteriores no son confiables (al menos en Postgres) porque algunas consultas de DB que se ejecutan en la segunda vez pueden ser significativamente más rápidas que en la primera, gracias a la caché de DB. Y desafortunadamente no hay una manera fácil de deshabilitar el caché en Postgres para hacer que estos puntos de referencia sean confiables.


0

Junto con el uso RANDOM(), también puede incluir esto en un ámbito:

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

O, si no le gusta eso como un alcance, simplemente tírelo a un método de clase. Ahora Thing.randomfunciona junto con Thing.random(n).


0

Dependiendo del significado de "aleatorio" y de lo que realmente quieres hacer, take podría ser suficiente.

Por "significado" de aleatorio quiero decir:

  • ¿Te refieres a darme algún elemento que no me importe es su posición? entonces es suficiente.
  • Ahora, si quieres decir "dame algún elemento con una probabilidad razonable de que los experimentos repetidos me den elementos diferentes del conjunto", entonces, fuerza la "Suerte" con cualquiera de los métodos mencionados en las otras respuestas.

Por ejemplo, para las pruebas, los datos de muestra podrían haberse creado aleatoriamente de todos modos, por lo que takees más que suficiente y, para ser sincero, incluso first.

https://guides.rubyonrails.org/active_record_querying.html#take

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.