Registro aleatório no ActiveRecord


151

Estou precisando obter um registro aleatório de uma tabela via ActiveRecord. Eu segui o exemplo de Jamis Buck de 2006 .

No entanto, também deparei com outra maneira através de uma pesquisa no Google (não é possível atribuir um link devido a novas restrições de usuário):

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

Estou curioso para saber como outras pessoas o fizeram ou se alguém sabe de que maneira seria mais eficiente.


2
2 pontos que podem ajudar a responder. 1. Qual a distribuição uniforme dos seus IDs, eles são seqüenciais? 2. Quão aleatório ele precisa ser? Aleatório bom o suficiente ou aleatório real?
22210 Michael

Eles são identificações seqüenciais geradas automaticamente pelo registro do ativo e precisam ser boas o suficiente.
Jyunderwood

1
Então sua solução proposta está próxima do ideal :) Eu usaria "SELECT MAX (id) FROM nome_tabela" em vez de COUNT (*), pois ele lidará com as linhas excluídas um pouco melhor, caso contrário, o resto é bom. Em resumo, se "bom o suficiente" estiver ok, você precisará apenas de um método que assuma uma distribuição próxima à que você realmente tem. Se é uniforme e, como você disse, o rand simples funciona muito bem.
22610 Michael

1
Isso não funcionará quando você excluir as linhas.
Venkat D.

Respostas:


136

Não encontrei a maneira ideal de fazer isso sem pelo menos duas consultas.

A seguir, utiliza-se um número gerado aleatoriamente (até a contagem atual de registros) como deslocamento .

offset = rand(Model.count)

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

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

Para ser sincero, acabei de usar ORDER BY RAND () ou RANDOM () (dependendo do banco de dados). Não é um problema de desempenho se você não tiver um problema de desempenho.


2
O código Model.find(:offset => offset).firstgerará erro. eu acho queModel.first(:offset => offset) pode ter um desempenho melhor.
Harish Shetty

1
Sim, eu tenho trabalhado com o Rails 3 e fico confuso sobre os formatos de consulta entre versões.
Toby Hede

7
Observe que o uso do deslocamento é muito lento com um grande conjunto de dados, pois ele realmente precisa da verificação de índice (ou verificação de tabela, caso o índice clusterizado seja usado como o InnoDB). Em outras palavras, é operação O (N), mas "WHERE id> = # {rand_id} ORDER BY id ASC LIMIT 1" é O (log N), que é muito mais rápido.
kenn

15
Esteja ciente de que a abordagem de deslocamento produz apenas um único ponto de dados encontrado aleatoriamente (o primeiro, depois de todos ainda são classificados por ID). Se você precisar de vários registros selecionados aleatoriamente, deverá usar essa abordagem várias vezes ou usar o método de ordem aleatória fornecido pelo seu banco de dados, ou seja, Thing.order("RANDOM()").limit(100)para 100 entradas selecionadas aleatoriamente. (Esteja ciente de que está RANDOM()no PostgreSQL e RAND()no MySQL ... não tão portátil quanto você gostaria que fosse.)
Florian Pilz

3
Não funciona para mim no Rails 4. Use Model.offset(offset).first.
mahemoff

206

Trilhos 6

Conforme afirma Jason nos comentários, no Rails 6, argumentos não relacionados a atributos não são permitidos. Você deve agrupar o valor em uma Arel.sql()instrução

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

Trilhos 5, 4

Nos Rails 4 e 5 , usando Postgresql ou SQLite , usando RANDOM():

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

Presumivelmente, o mesmo funcionaria para o MySQL comRAND()

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

Isso é cerca de 2,5 vezes mais rápido que a abordagem na resposta aceita .

Advertência : isso é lento para grandes conjuntos de dados com milhões de registros, portanto, você pode querer adicionar uma limitcláusula.


4
"Random ()" também funciona no sqlite, portanto, para aqueles que ainda desenvolvem no sqlite e executam o postgres na produção, sua solução funciona nos dois ambientes.
Wuliwong

5
Eu criei uma referência para isso contra a resposta aceita. No Postgresql 9.4, a abordagem desta resposta é duas vezes mais rápida.
panmari

3
Parece que não é recomendado no mysql webtrenches.com/post.cfm/avoid-rand-in-mysql
Prakash Murthy

Esta é a solução mais rápida
Sergio Belevskij

1
"Argumentos que não são de atributos serão proibidos no Rails 6.0. Este método não deve ser chamado com valores fornecidos pelo usuário, como parâmetros de solicitação ou atributos de modelo. Valores conhecidos como seguros podem ser transmitidos envolvendo-os em Arel.sql ()."
Trenton Tyler

73

Seu código de exemplo começará a se comportar de maneira imprecisa quando os registros forem excluídos (favorecerá injustamente itens com IDs mais baixos)

Você provavelmente está melhor usando os métodos aleatórios no seu banco de dados. Isso varia dependendo do banco de dados que você está usando, mas: order => "RAND ()" funciona no mysql e: order => "RANDOM ()" funciona no postgres

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

7
ORDER BY RAND () para MySQL termina em tempo de execução horrível à medida que os dados aumentam. É insustentável (dependendo dos requisitos de tempo), começando em apenas milhares de linhas.
285 Michael

Michael traz um grande ponto (isso também é válido para outros bancos de dados). Geralmente, selecionar linhas aleatórias de tabelas grandes não é algo que você deseja fazer em uma ação dinâmica. O armazenamento em cache é seu amigo. Repensar o que você está tentando realizar também pode não ser uma má idéia.
Semanticart

1
Encomendar RAND () no mysql em uma tabela com cerca de um milhão de linhas é slooooooooooooooooooooooow.
Subimage

24
Não funciona mais. Use em Model.order("RANDOM()").firstvez disso.
Phil Pirozhkov

Lento e específico do banco de dados. O ActiveRecord deve funcionar perfeitamente entre os bancos de dados, portanto você não deve usar esse método.
Dex1

29

Comparando esses dois métodos no MySQL 5.1.49, Ruby 1.9.2p180 em uma tabela de produtos com + 5 milhões 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)

O deslocamento no MySQL parece ser muito mais lento.

EDIT Eu também tentei

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

Mas eu tive que matá-lo depois de 60 segundos. O MySQL estava "Copiando para a tabela tmp no disco". Isso não vai funcionar.


1
Para aqueles que procuram mais testes, quanto tempo leva uma abordagem aleatória real: tentei Thing.order("RANDOM()").firstem uma tabela com entradas de 250k - a consulta terminou em meio segundo. (PostgreSQL 9.0, REE 1.8.7, núcleos de 2 x 2,66 GHz) Isso é rápido o suficiente para mim, pois estou fazendo uma "limpeza" única.
Florian Pilz

6
O método rand do Ruby retorna um a menos que o número especificado para que você deseje rand_id = rand(Product.count) + 1ou nunca obtenha o último registro.
Ritchie

4
A nota random1não funcionará se você excluir uma linha da tabela. (A contagem será menor que o ID máximo e você nunca poderá selecionar linhas com IDs altos).
Nicholas

O uso random2pode ser aprimorado #orderusando uma coluna indexada.
Carson Reinke

18

Não precisa ser tão difícil.

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

pluckretorna uma matriz de todos os IDs na tabela. O samplemétodo na matriz retorna um ID aleatório da matriz.

Isso deve ter um bom desempenho, com igual probabilidade de seleção e suporte para tabelas com linhas excluídas. Você pode até misturá-lo com restrições.

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

E, assim, escolha um usuário aleatório que goste de sextas-feiras, em vez de qualquer usuário.


8
Isso é limpo e funciona para uma mesa pequena ou para uso único, apenas observe que não será dimensionado. Em uma mesa da 3M, obter IDs leva cerca de 15 segundos para mim no MariaDB.
mahemoff

2
Este é um bom ponto. Você encontrou uma solução alternativa mais rápida, mantendo as mesmas qualidades?
Niels B.

A solução de offset aceita não mantém as mesmas qualidades?
Mahemoff

Não, ele não suporta condições e não possui uma probabilidade igual de seleção para tabelas com registros excluídos.
Niels B.

1
Pense bem: se você aplicar as restrições ao contar e selecionar com um deslocamento, a técnica deve funcionar. Eu estava imaginando apenas aplicá-lo na contagem.
Niels B.

15

Não é recomendável usar essa solução, mas se, por algum motivo, você realmente deseja selecionar aleatoriamente um registro enquanto faz apenas uma consulta ao banco de dados, você pode usar o samplemétodo da classe Ruby Array , que permite selecionar um item aleatório de uma matriz.

Model.all.sample

Esse método requer apenas consulta ao banco de dados, mas é significativamente mais lento que alternativas, como as Model.offset(rand(Model.count)).firstque requerem duas consultas ao banco de dados, embora a última ainda seja a preferida.


99
Não faça isso. Sempre.
Zabba 16/10/12

5
Se você tiver 100 mil linhas no seu banco de dados, todas elas deverão ser carregadas na memória.
precisa

3
É claro que não é recomendado para código em tempo real de produção, mas eu gosto dessa solução, é muito claro para situações especiais, como a propagação do banco de dados com valores falsos.
Fevillen

13
Por favor - nunca diga nunca. Essa é uma ótima solução para depuração em tempo de desenvolvimento, se a tabela for pequena. (E se você estiver colhendo amostras, a depuração é possivelmente o caso de uso).
mahemoff

Estou usando para semear e é bom para mim. Além disso, Model.all.sample (n) também funciona :)
Arnaldo Ignacio Gaspar Véjar

13

Eu fiz uma jóia de trilhos 3 para lidar com isso:

https://github.com/spilliton/randumb

Ele permite que você faça coisas assim:

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

7
Na documentação desta jóia, eles explicam "o randumb simplesmente adiciona um adicional ORDER BY RANDOM()(ou RAND()mysql) à sua consulta". - portanto, os comentários sobre o mau desempenho mencionados nos comentários à resposta por @semanticart também se aplicam ao usar esta gema. Mas pelo menos é independente de DB.
Nicolas

8

Eu uso isso tantas vezes no console, estendo o ActiveRecord em um inicializador - exemplo do Rails 4:

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

Posso ligar Foo.randompara trazer de volta um registro aleatório.


1
você precisa limit(1)? ActiveRecord#firstdeve ser inteligente o suficiente para fazer isso.
tokland

6

Uma consulta no Postgres:

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

Usando um deslocamento, duas consultas:

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

1
Não precisa de -1, rand conta até num - 1
anemaria20

Obrigado, alterado: +1:
Thomas Klemm

5

Ler tudo isso não me deu muita confiança sobre qual deles funcionaria melhor em minha situação particular com o Rails 5 e o MySQL / Maria 5.5. Então, testei algumas das respostas em ~ 65000 registros e tenho duas alternativas:

  1. RAND () com a limité um vencedor claro.
  2. Não 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 resposta sintetiza, valida e atualiza a resposta de Mohamed , bem como o comentário de Nami WANG sobre o mesmo e o comentário de Florian Pilz sobre a resposta aceita - envie votos a eles!


3

Você pode usar o Arraymétodo sample, o método sampleretorna um objeto aleatório de uma matriz. Para usá-lo, basta executar em uma ActiveRecordconsulta simples que retorna uma coleção, por exemplo:

User.all.sample

retornará algo como isto:

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

Eu não recomendaria trabalhar com métodos de matriz enquanto estiver usando o AR. Dessa maneira, leva quase 8 vezes o tempo order('rand()').limit(1)que o trabalho "é o mesmo" (com ~ 10 mil registros).
23618 Sebastian Palma

3

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

insira a descrição da imagem aqui

  1. a offsetabordagem de resposta aceita custa 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.


2

Se você precisar selecionar alguns resultados aleatórios dentro do escopo 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

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 }

Essa será uma consulta se o tamanho do modelo já estiver armazenado em cache e duas caso contrário.


1

Rails 4.2 e Oracle :

Para a oracle, você pode definir um escopo no seu modelo da seguinte maneira:

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

ou

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

E então, para uma amostra, chame assim:

Model.random_order.take(10)

ou

Model.random_order.limit(5)

é claro que você também pode fazer um pedido sem um escopo como este:

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

Você pode fazer isso também com o postgres with order('random()'e MySQL order('rand()'). Esta é definitivamente a melhor resposta.
Jrochkind

1

Para o banco de dados MySQL, tente: Model.order ("RAND ()"). First


Isso não funciona no mysql .. você deve incluir pelo menos com qual mecanismo de banco de dados isso deve funcionar #
Arnold Roa

Desculpe, houve um erro de digitação. Corrigido agora. Deve trabalhar para mysql (somente)
Vadim Eremeev

1

Se você estiver usando o PostgreSQL 9.5+, poderá tirar proveito de TABLESAMPLE para selecionar um registro aleatório.

Os dois métodos de amostragem padrão ( SYSTEMe BERNOULLI) exigem que você especifique o número de linhas a serem retornadas como uma porcentagem do número total de linhas na tabela.

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

Isso requer conhecer a quantidade de registros na tabela para selecionar a porcentagem apropriada, o que pode não ser fácil de encontrar rapidamente. Felizmente, existe o tsm_system_rowsmódulo que permite especificar o número de linhas a serem retornadas diretamente.

CREATE EXTENSION tsm_system_rows;

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

Para usar isso no ActiveRecord, primeiro habilite a extensão dentro de uma migração:

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

Em seguida, modifique a fromcláusula da consulta:

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

Não sei se o SYSTEM_ROWSmétodo de amostragem será inteiramente aleatório ou se ele retorna a primeira linha de uma página aleatória.

A maioria dessas informações foi retirada de um post do 2ndQuadrant escrito por Gulcin Yildirim .


1

Depois de ver tantas respostas, decidi compará-las todas no meu banco de dados PostgreSQL (9.6.3). Eu uso uma tabela menor de 100.000 e me livrei da Model.order ("RANDOM ()"). Primeiro, pois ela já era duas ordens de magnitude mais lenta.

Usando uma tabela com 2.500.000 entradas com 10 colunas, o vencedor foi o método de arranque quase 8 vezes mais rápido que o vice-campeão (deslocamento. Eu apenas o executei em um servidor local para que o número possa ser inflado, mas é maior o suficiente para que o arranque É o método que eu vou acabar usando.Também vale a pena notar que isso pode causar problemas: você obtém mais de um resultado por vez, já que cada um deles será único, menos aleatório.

O Pluck ganha rodando 100 vezes na minha tabela de 25.000.000 de linhas. Contudo; é preciso uma quantidade razoável 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)

Aqui estão os dados em execução 2000 vezes na minha tabela de 100.000 linhas para descartar aleatoriamente

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

Pergunta muito antiga, mas com:

rand_record = Model.all.shuffle

Você tem uma matriz de registro, classificar por ordem aleatória. Não há necessidade de gemas ou scripts.

Se você deseja um registro:

rand_record = Model.all.shuffle.first

1
Não é a melhor opção, pois isso carrega todos os registros na memória. Além disso, shuffle.first==.sample
Andrew Rozhenko 1/10/19

0

Sou novato no RoR, mas consegui que isso funcionasse para mim:

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

Veio de:

Como ordenar (embaralhar) aleatoriamente uma matriz em Ruby?


4
O ruim é que ele carregará todos os cartões do banco de dados. É mais eficiente fazê-lo dentro do banco de dados.
Anton Kuzmin

Você também pode embaralhar matrizes com array.shuffle. De qualquer forma, cuidado, pois Card.allcarregará todos os registros do cartão na memória, o que torna mais ineficiente quanto mais objetos estamos falando.
Thomas Klemm

0

Que tal fazer:

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

Para mim é muito claro


0

Eu tento este exemplo do Sam no meu aplicativo usando os trilhos 4.2.8 do Benchmark (coloquei 1..Category.count aleatoriamente, porque se o aleatório receber um 0, ele produzirá um erro (ActiveRecord :: RecordNotFound: Não foi possível encontrar Categoria com 'id' = 0)) e a mina era:

 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)parece elegante, mas é lento para tabelas grandes porque precisa buscar e classificar todas as linhas, mesmo que limitseja 1 (internamente no banco de dados, mas não no Rails). Não tenho certeza sobre o MySQL, mas isso acontece no Postgres. Mais explicações aqui e aqui .

Uma solução para tabelas grandes é .from("products TABLESAMPLE SYSTEM(0.5)") onde 0.5significa 0.5%. No entanto, acho que essa solução ainda é lenta se você tiver WHEREcondições que filtram muitas linhas. Eu acho que é porque TABLESAMPLE SYSTEM(0.5)busca todas as linhas antes que as WHEREcondições se apliquem.

Outra solução para tabelas grandes (mas não muito aleatória) é:

products_scope.limit(sample_size).sample(limit)

onde sample_sizepode estar100 (mas não muito grande, caso contrário, é lento e consome muita memória) e limitpode estar 1. Observe que, embora isso seja rápido, mas não seja realmente aleatório, é aleatório sample_sizeapenas nos registros.

PS: Os resultados de benchmark nas respostas acima não são confiáveis ​​(pelo menos no Postgres) porque algumas consultas ao banco de dados em execução no 2º tempo podem ser significativamente mais rápidas do que na 1ª vez, graças ao cache do banco de dados. E, infelizmente, não há uma maneira fácil de desativar o cache no Postgres para tornar esses benchmarks confiáveis.


0

Junto com o uso RANDOM(), você também pode colocar isso em um escopo:

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

Ou, se você não gosta disso como escopo, jogue-o em um método de classe. Agora Thing.randomfunciona junto com Thing.random(n).


0

Dependendo do significado de "aleatório" e do que você realmente deseja fazer, takepode ser suficiente.

Com o "significado" de aleatório, quero dizer:

  • Quer dizer, me dê algum elemento que eu não me importo com a posição? então é o suficiente.
  • Agora, se você quer dizer "me dê qualquer elemento com uma probabilidade razoável de que experimentos repetidos me darão elementos diferentes do conjunto", force a "Sorte" com qualquer um dos métodos mencionados nas outras respostas.

Por exemplo, para testes, os dados de amostra poderiam ter sido criados aleatoriamente de qualquer maneira, portanto, takeé mais do que suficiente e, para ser sincero, até first.

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

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.