Rails 3: Obter registro aleatório


132

Então, eu encontrei vários exemplos para encontrar um registro aleatório no Rails 2 - o método preferido parece ser:

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

Sendo um novato, não tenho certeza de como isso pode ser construído usando a nova sintaxe de localização no Rails 3.

Então, qual é o "Rails 3 Way" para encontrar um registro aleatório?



9
^^ exceto que estou procurando especificamente a melhor maneira do Rails 3, que é o objetivo da questão.
Andrew

Rails 3 específico só é consulta cadeia :)
fl00r

Respostas:


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

Na verdade, no Rails 3 todos os exemplos funcionarão. Mas o uso da ordem RANDOMé bastante lento para grandes tabelas, mas com mais estilo sql

UPD. Você pode usar o seguinte truque em uma coluna indexada (sintaxe do PostgreSQL):

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

11
Seu primeiro exemplo não funcionará no MySQL embora - a sintaxe para MySQL é Thing.first (: order => "RAND ()") (perigo de escrever SQL em vez de usar as abstrações ActiveRecord)
DanSingerman

@ DanSingerman, sim, é DB específico RAND()ou RANDOM(). Graças
fl00r

E isso não criará problemas se houver itens ausentes no índice? (se algo no meio da pilha for excluído, haverá uma chance de isso ser solicitado? #
Victor S

@ Victor, não, não vai #offset apenas vai para o próximo registro disponível. Eu testei com Ruby 1.9.2 e Rails 3.1
SooDesuNe

1
@JohnMerlino, sim 0 é compensado, não id. Deslocamento 0 significa o primeiro item de acordo com a ordem.
fl00r

29

Estou trabalhando em um projeto ( Rails 3.0.15, ruby ​​1.9.3-p125-perf ) em que o banco de dados está no host local e a tabela de usuários possui um pouco mais de 100 mil registros .

Usando

encomendar por RAND ()

é bem lento

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

torna-se

SELECT users. * DA usersORDEM POR RAND (id) LIMITE 1

e leva de 8 a 12 segundos para responder !!

Log do Rails:

Carga do usuário (11030.8ms) SELECT users. * DA usersORDEM POR RAND () LIMITE 1

da explicação do 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 |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+

Você pode ver que nenhum índice é usado ( Chaves_Possíveis = NULL ), uma tabela temporária é criada e uma passagem extra é necessária para buscar o valor desejado ( extra = Usando temporário; Usando filesort ).

Por outro lado, dividindo a consulta em duas partes e usando Ruby, temos uma melhoria razoável no tempo de resposta.

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

(; nada para uso do console)

Log do Rails:

ID de SELEÇÃO de Carga do Usuário (25,2 ms) DE SELEÇÃO de usersCarga do Usuário (0,2 ms) users. * FROM usersWHERE users. id= 106854 LIMITE 1

e a explicação do mysql prova o porquê:

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

agora podemos usar apenas índices e a chave primária e executar o trabalho cerca de 500 vezes mais rápido!

ATUALIZAR:

como apontado por icantbecool nos comentários, a solução acima apresenta uma falha se houver registros excluídos na tabela.

Uma solução alternativa para isso pode ser

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

que se traduz em duas consultas

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

e roda em cerca de 500ms.


adicionar ".id" depois de "last" ao seu segundo exemplo evitará o erro "não foi possível encontrar o modelo sem ID". Por exemplo, User.find (users.first (Random.rand (users.length)). Last.id)
turing_machine

Aviso! No MySQL, RAND(id)você não terá uma ordem aleatória diferente a cada consulta. Use RAND()se desejar uma ordem diferente para cada consulta.
perfil completo de Justin Tanner

O User.find (users.first (Random.rand (users.length)). Last.id) não funcionará se houver um registro excluído. [1,2,4,5] e potencialmente poderia escolher o ID 3, mas não haveria uma relação de registro ativa.
icantbecool

Além disso, users = User.scoped.select (: id); nil não é preterido. Utilize este em vez disso: users = User.where (nil) .Select (: id)
icantbecool

Acredito que o uso de Random.rand (users.length) como parâmetro para o primeiro seja um bug. Random.rand pode retornar 0. Quando 0 é usado como parâmetro para o primeiro, o limite é definido como zero e isso não retorna registros. Em vez disso, o que se deve usar é 1 + Aleatório (users.length) assumindo users.length> 0.
SWoo

12

Se estiver usando o Postgres

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

Se estiver usando MySQL

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

Nos dois casos, você seleciona 5 registros aleatoriamente na tabela Usuários. Aqui está a consulta SQL real exibida no console.

SELECT * FROM users ORDER BY RANDOM() LIMIT 5

11

Fiz uma pedra preciosa rails 3 para fazer isso com melhor desempenho em tabelas grandes e permitir que você encadeie relações e escopos:

https://github.com/spilliton/randumb

(editar): O comportamento padrão da minha jóia basicamente usa a mesma abordagem acima agora, mas você tem a opção de usar da maneira antiga, se quiser :)


6

Muitas das respostas postadas na verdade não apresentam bom desempenho em tabelas bastante grandes (mais de 1 milhão de linhas). A ordenação aleatória demora rapidamente alguns segundos e a contagem na mesa também leva bastante tempo.

Uma solução que funciona bem para mim nessa situação é usar RANDOM()com uma condição where:

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

Em uma tabela com mais de um milhão de linhas, essa consulta geralmente leva menos de 2ms.


Outras vantagens da sua solução é usar a takefunção que fornece LIMIT(1)consulta, mas retorna um único elemento em vez de matriz. Portanto, não precisamos invocar #first
Piotr Galas

Parece-me que os registros no início da tabela têm maior probabilidade de serem selecionados dessa maneira, o que pode não ser o que você deseja alcançar.
Gorn 17/07/19

5

aqui vamos nós

trilhos maneira

#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

ou o segundo pensamento é

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

uso:

Model.random #returns shuffled collection

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

se não houver usuários e você desejar obter 2, ocorrerá erros. faz sentido.
Tim Kretschmer

1
A segunda abordagem não funcionará com postgres, mas você pode usar "RANDOM()"em vez disso ...
Daniel Richter

4

Isso foi muito útil para mim, no entanto, eu precisava de um pouco mais de flexibilidade, então foi isso que fiz:

Case1: Localizando uma fonte de registro aleatório : trevor turk site
Adicione isso ao 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

então no seu controlador você pode chamar algo assim

@thing = Thing.random

Case2: Encontrar vários registros aleatórios (sem repetições) fonte: não consigo lembrar
Eu precisava encontrar 10 registros aleatórios sem repetições, então foi o que eu achei que funcionou
No seu controlador:

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

Isso encontrará 10 registros aleatórios, no entanto, vale ressaltar que, se o banco de dados for particularmente grande (milhões de registros), isso não seria o ideal e o desempenho será prejudicado. É vai executar bem até alguns milhares de registros, o que foi suficiente para mim.


4

O método Ruby para escolher aleatoriamente um item de uma lista é sample. Querendo criar um eficiente samplepara o ActiveRecord, e com base nas respostas anteriores, usei:

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

Eu coloquei isso lib/ext/sample.rbe carreguei com isso em config/initializers/monkey_patches.rb:

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

Na verdade, #countfará uma chamada para o banco de dados para a COUNT. Se o registro já estiver carregado, isso pode ser uma má ideia. Um refator seria o uso, #sizepois ele decidirá se #countdeve ser usado ou, se o registro já estiver carregado, o uso #length.
BenMorganIO

Mudou de countpara com sizebase nos seus comentários. Mais informações em: dev.mensfeld.pl/2014/09/…
Dan Kohn

3

Funciona no Rails 5 e é independente do DB:

Isso no seu controlador:

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

Obviamente, você pode colocar isso em uma preocupação, como mostrado aqui .

app / modelos / preocupações / 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

então...

app / models / book.rb

class Book < ActiveRecord::Base
  include Randomable
end

Então você pode usar simplesmente fazendo:

Books.random

ou

Books.random(3)

Isso sempre leva registros subsequentes, que precisam ser pelo menos documentados (pois pode não ser o que o usuário deseja).
Gorn

2

Você pode usar sample () no ActiveRecord

Por exemplo

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

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


33
Essa é uma consulta muito ruim para usar se você tiver uma grande quantidade de registros, como o banco de dados selecionará TODOS os registros, o Rails escolherá cinco registros a partir disso - um desperdício maciço.
precisa saber é o seguinte

5
samplenão está no ActiveRecord, a amostra está na matriz. api.rubyonrails.org/classes/Array.html#method-i-sample
Frans

3
Essa é uma maneira cara de obter um registro aleatório, especialmente em uma tabela grande. O Rails carregará um objeto para cada registro da sua tabela na memória. Se você precisar de provas, execute 'rails console', tente 'SomeModelFromYourApp.find (: all) .sample (5)' e veja o SQL produzido.
Eliot Sykes

1
Veja minha resposta, que transforma essa resposta cara em uma beleza simplificada para obter vários registros aleatórios.
Arcolye

1

Se estiver usando o Oracle

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

Resultado

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

1

Recomenda vivamente esta gema para registros aleatórios, projetados especialmente para tabelas com muitas linhas de dados:

https://github.com/haopingfan/quick_random_records

Todas as outras respostas apresentam um desempenho ruim com um banco de dados grande, exceto esta gema:

  1. quick_random_records custam apenas 4.6mstotalmente.

insira a descrição da imagem aqui

  1. o User.order('RAND()').limit(10)custo de resposta aceito 733.0ms.

insira a descrição da imagem aqui

  1. a offsetabordagem custou 245.4mstotalmente.

insira a descrição da imagem aqui

  1. o User.all.sample(10)custo da abordagem 573.4ms.

insira a descrição da imagem aqui

Nota: Minha tabela possui apenas 120.000 usuários. Quanto mais registros você tiver, mais enorme será a diferença de desempenho.


ATUALIZAR:

Desempenho na tabela com 550.000 linhas

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

insira a descrição da imagem aqui

  1. gem: quick_random_recordssó custa 6.4mstotalmente

insira a descrição da imagem aqui


-2

Uma maneira muito fácil de obter vários registros aleatórios da tabela. Isso faz duas consultas baratas.

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

Você pode alterar o "3" para o número de registros aleatórios que desejar.


1
não, a parte Model.pluck (: id) .sample (3) não é barata. Ele lerá o campo de identificação para todos os elementos da tabela.
Maximiliano Guzman

Existe uma maneira mais rápida e independente de banco de dados?
Arcolye

-5

Acabei de encontrar este problema desenvolvendo um pequeno aplicativo em que desejava selecionar uma pergunta aleatória do meu banco de dados. Eu usei:

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

E está funcionando bem para mim. Não posso falar sobre como o desempenho de bancos de dados maiores, pois esse é apenas um aplicativo pequeno.


Sim, isso está apenas obtendo todos os seus registros e usando métodos de matriz ruby ​​neles. A desvantagem é, obviamente, que isso significa carregar todos os seus registros na memória, reordená-los aleatoriamente e, em seguida, pegar o segundo item na matriz reordenada. Definitivamente, isso poderia ser um problema de memória se você estivesse lidando com um grande conjunto de dados. Além disso, por que não pegar o primeiro elemento? (ie. shuffle[0])
Andrew

deve ser aleatório [0]
Marcelo Austria
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.