ruby on rails - Registro aleatorio en ActiveRecord
ruby-on-rails random (21)
Necesito obtener un registro aleatorio de una tabla a través de ActiveRecord. He seguido el ejemplo de Jamis Buck desde 2006 .
Sin embargo, también he encontrado otro camino 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 de cómo otros aquí lo han hecho o si alguien sabe de qué manera sería más eficiente.
Comparando estos dos métodos en MySQL 5.1.49, Ruby 1.9.2p180 en una tabla de productos con más de 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.
EDIT También probé
Product.first(:order => "RAND()")
Pero tuve que matarlo después de ~ 60 segundos. MySQL era "Copiar a la tabla tmp en el disco". Eso no va a funcionar.
Después de ver tantas respuestas, decidí compararlas en mi base de datos PostgreSQL (9.6.3). Utilizo una tabla 100,000 más pequeña y me deshice del Model.order ("RANDOM ()") primero porque ya eran dos órdenes de magnitud más lentos.
Utilizando una tabla con 2,500,000 entradas con 10 columnas, el ganador fue el método de arranque casi 8 veces más rápido que el finalista (compensación. Solo ejecuté esto en un servidor local para que el número pudiera ser inflado pero es lo suficientemente grande para que el pluck método es lo que voy a terminar usando. También vale la pena señalar que esto podría causar problemas si arranca más de 1 resultado a la vez, ya que cada uno de ellos será único, también conocido como menos aleatorio.
Pluck gana al ejecutar 100 veces en mi tabla de filas de 25,000,000 Editar: en realidad esta vez incluye el pluck in the loop si lo saco, funciona tan rápido como la iteración simple en el id. Sin embargo; requiere 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 corriendo 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)
El método Ruby para elegir aleatoriamente un artículo de una lista es sample
. Queriendo crear una sample
eficiente para 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 en lib/ext/sample.rb
y luego lo lib/ext/sample.rb
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á almacenado en la memoria caché y dos si no.
En Rails 4 y 5 , usando Postgresql o SQLite , usando RANDOM()
:
Model.order("RANDOM()").first
Presumiblemente, lo mismo funcionaría para MySQL con RAND()
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 cláusula de limit
.
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)
Intento esto del ejemplo de Sam en mi aplicación utilizando los carriles 4.2.8 de Benchmark (pongo 1..Category.count por aleatorio, porque si el azar toma un 0 producirá un error (ActiveRecord :: RecordNotFound: Could not find) 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)
Leer todo esto no me dio mucha confianza sobre cuál de estos 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 obtuve dos respuestas:
- RAND () con un
limit
es un claro ganador. - 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 el mismo y el comentario de Florian Pilz sobre la respuesta aceptada. ¡Por favor envíales votos!
Lo uso muy a menudo desde la consola 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
Luego puedo llamar a Foo.random
para traer un registro aleatorio.
No he encontrado una forma ideal de hacerlo sin al menos dos consultas.
A continuación, se usa un número generado aleatoriamente (hasta el recuento de registros actual) como un desplazamiento .
offset = rand(Model.count)
# Rails 4
rand_record = Model.offset(offset).first
# Rails 3
rand_record = Model.first(:offset => offset)
Para ser sincero, acabo de usar ORDER BY RAND () o RANDOM () (dependiendo de la base de datos). No es un problema de rendimiento si no tienes un problema de rendimiento.
No se recomienda que use esta solución, pero si por alguna razón realmente desea seleccionar aleatoriamente un registro mientras solo hace una consulta de base de datos, puede usar el método de sample
de la clase Ruby Array , que le permite seleccionar un elemento de una matriz.
Model.all.sample
Este método requiere solo la consulta de la base de datos, pero es significativamente más lento que alternativas como Model.offset(rand(Model.count)).first
que requieren dos consultas de bases de datos, aunque este último todavía es preferido.
No tiene que ser tan difícil.
ids = Model.pluck(:id)
random_model = Model.find(ids.sample)
pluck
devuelve una matriz de todas las identificaciones de la tabla. El método de sample
en la matriz, devuelve una identificación aleatoria de la matriz.
Esto debería funcionar bien, con igual 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.
Para la base de datos MySQL, pruebe: Model.order ("RAND ()") primero
Puede usar el sample
método Array
, el sample
método devuelve un objeto aleatorio de una matriz, para poder usarlo solo necesita ejecutar en una consulta simple de ActiveRecord
que devuelve una colección, por ejemplo:
User.all.sample
devolverá algo como esto:
#<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">
Qué hacer?
rand_record = Model.find(Model.pluck(:id).sample)
Para mí está muy claro
Si está utilizando PostgreSQL 9.5+, puede aprovechar TABLESAMPLE
para seleccionar un registro aleatorio.
Los dos métodos de muestreo predeterminados ( SYSTEM
y 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 módulo tsm_system_rows
que le permite especificar el número de filas a 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 cláusula from
de la consulta:
customer = Customer.from("customers TABLESAMPLE SYSTEM_ROWS(1)").first
No sé si el método de muestreo SYSTEM_ROWS
será completamente aleatorio o si solo devuelve la primera fila de una página aleatoria.
La mayoría de esta información se tomó de una publicación de blog de 2ndQuadrant escrita por Gulcin Yildirim .
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)
Soy nuevo en RoR pero tengo esto para trabajar para mí:
def random
@cards = Card.all.sort_by { rand }
end
Vino de:
¿Cómo ordenar aleatoriamente (codificar) una matriz en Ruby?
Su código de ejemplo comenzará a comportarse de manera incorrecta una vez que se eliminen los registros (favorecerá injustamente los elementos con id. Más bajos)
Probablemente sea mejor que uses los métodos aleatorios dentro de tu base de datos. Estos varían dependiendo de qué DB está utilizando, pero: order => "RAND ()" funciona para mysql y: order => "RANDOM ()" funciona para postgres
Model.first(:order => "RANDOM()") # postgres example
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)
.order(''RANDOM()'').limit(limit)
ve limpio, pero es lento para tablas grandes porque necesita buscar y ordenar todas las filas incluso si el limit
es 1 (internamente en la base de datos pero no en Rails). No estoy seguro acerca de MySQL, pero esto sucede en Postgres. Más explicación here y here .
Una solución para tablas grandes es .from("products TABLESAMPLE SYSTEM(0.5)")
donde 0.5
significa 0.5%
. Sin embargo, creo que esta solución aún es lenta si tiene WHERE
condiciones que filtran muchas filas. Supongo que es porque TABLESAMPLE SYSTEM(0.5)
busca todas las filas antes de que se apliquen las condiciones.
Otra solución para tablas grandes (pero no muy aleatorias) es:
products_scope.limit(sample_size).sample(limit)
donde sample_size
puede ser 100
(pero no demasiado grande; de lo contrario, es lento y consume mucha memoria), y el limit
puede ser 1
. Tenga en cuenta que, aunque esto es rápido pero no es realmente aleatorio, es aleatorio solo dentro de los registros sample_size
.
PD: los resultados de referencia en las respuestas anteriores no son confiables (al menos en Postgres) porque algunas consultas DB que se ejecutan en la segunda vez pueden ser significativamente más rápidas que la ejecución en la primera vez, gracias a la memoria caché de la base de datos. Y desafortunadamente no hay una manera fácil de desactivar el caché en Postgres para que estos puntos de referencia sean confiables.
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 puedes hacer un pedido sin un alcance como ese:
Model.all.order(''DBMS_RANDOM.RANDOM'') # or DBMS_RANDOM.VALUE respectively