ruby-on-rails activerecord random

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 .



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:

  1. RAND () con un limit es 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 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)



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