Como extrair um registro aleatório usando o ORM do Django?

176

Eu tenho um modelo que representa pinturas que apresento no meu site. Na página principal, eu gostaria de mostrar alguns deles: o mais novo, um que não foi visitado por mais tempo, o mais popular e o aleatório.

Estou usando o Django 1.0.2.

Embora os três primeiros sejam fáceis de usar usando modelos de django, o último (aleatório) me causa alguns problemas. Eu posso ofc codificá-lo na minha opinião, para algo como isto:

number_of_records = models.Painting.objects.count()
random_index = int(random.random()*number_of_records)+1
random_paint = models.Painting.get(pk = random_index)

Não parece algo que eu gostaria de ter - isso faz parte da abstração do banco de dados e deve estar no modelo. Além disso, aqui eu preciso cuidar dos registros removidos (o número de todos os registros não me cobrirá todos os valores-chave possíveis) e provavelmente muitas outras coisas.

Alguma outra opção como eu posso fazer isso, preferencialmente de alguma forma dentro da abstração do modelo?

kender
fonte
Como você exibe as coisas e quais são exibidas, faz parte do nível "Visualizar" ou da lógica de negócios que deve estar no nível "Controlador" do MVC, na minha opinião.
Gabriele D'Antona
No Django, o controlador é a visão. docs.djangoproject.com/en/dev/faq/general/…

Respostas:

169

O uso order_by('?')matará o servidor db no segundo dia de produção. Uma maneira melhor é algo como o descrito em Obtendo uma linha aleatória de um banco de dados relacional .

from django.db.models.aggregates import Count
from random import randint

class PaintingManager(models.Manager):
    def random(self):
        count = self.aggregate(count=Count('id'))['count']
        random_index = randint(0, count - 1)
        return self.all()[random_index]
Emil Ivanov
fonte
45
Quais são os benefícios de model.objects.aggregate(count=Count('id'))['count']overmodel.objects.all().count()
Ryan Saxe
11
Embora seja muito melhor que a resposta aceita, observe que essa abordagem faz duas consultas SQL. Se a contagem mudar no meio, pode ser possível obter um erro fora dos limites.
Nelo Mitranim
2
Esta é uma solução errada. Não funcionará se seus IDs não começarem a partir de 0. E também quando os IDs não forem contíguos. Digamos, o primeiro registro começa em 500 e o último é 599 (assumindo contiguidade). A contagem seria 54950. Certamente a lista [54950] não existe, porque o comprimento do seu consultor é 100. Ele lançará o índice fora da exceção vinculada. Não sei por que tantas pessoas aprovaram isso e isso foi marcado como resposta aceita.
Sajid
1
@ sajid: Por que exatamente você está me perguntando? É muito fácil ver a soma total de minhas contribuições para esta pergunta: editar um link para apontar para um arquivo após apodrecer. Eu nem votei em nenhuma das respostas. Mas acho divertido que essa resposta e a que você afirma ser muito melhor usem .all()[randint(0, count - 1)]com efeito. Talvez você deva se concentrar em identificar qual parte da resposta está errada ou fraca, em vez de redefinir "um por um erro" para nós e gritar com os eleitores tolos. (Talvez seja que ele não está usando .objects?)
Nathan Tuggy
3
@NathanTuggy. Ok, meu mal. Desculpe
Sajid
260

Basta usar:

MyModel.objects.order_by('?').first()

Está documentado na API QuerySet .

muhuk
fonte
71
Por favor, note que esta abordagem pode ser muito lento, conforme documentado :)
Nicolas Dumazet
6
"pode ​​ser caro e lento, dependendo do back-end do banco de dados que você está usando." - alguma experiência em diferentes backends de banco de dados? (sqlite / mysql / postgres)?
kender
4
Eu não testei, então isso é pura especulação: por que deveria ser mais lento do que recuperar todos os itens e executar a randomização em Python?
muhuk
8
Eu li que é lento no mysql, pois o mysql tem uma ordem aleatória incrivelmente ineficiente.
Brandon Henry
33
Por que não apenas random.choice(Model.objects.all())?
Jamey
25

As soluções com order_by ('?') [: N] são extremamente lentas, mesmo para tabelas de tamanho médio, se você usa o MySQL (não conhece outros bancos de dados).

order_by('?')[:N]será traduzido para SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT Nconsulta.

Isso significa que, para cada linha da tabela, a função RAND () será executada, a tabela inteira será classificada de acordo com o valor dessa função e os primeiros N registros serão retornados. Se suas mesas são pequenas, tudo bem. Mas na maioria dos casos, essa é uma consulta muito lenta.

Eu escrevi uma função simples que funciona mesmo se os ID tiverem buracos (algumas linhas foram excluídas):

def get_random_item(model, max_id=None):
    if max_id is None:
        max_id = model.objects.aggregate(Max('id')).values()[0]
    min_id = math.ceil(max_id*random.random())
    return model.objects.filter(id__gte=min_id)[0]

É mais rápido que order_by ('?') Em quase todos os casos.

Mikhail Korobov
fonte
30
Além disso, infelizmente, está longe de ser aleatório. Se você tiver um registro com o ID 1 e outro com o ID 100, ele retornará o segundo em 99% das vezes.
DS.
16

Aqui está uma solução simples:

from random import randint

count = Model.objects.count()
random_object = Model.objects.all()[randint(0, count - 1)] #single random object
Maulik Patel
fonte
10

Você pode criar um gerente em seu modelo para fazer esse tipo de coisa. Para entender primeiro o que um gerente é, o Painting.objectsmétodo é um gerente que contém all(), filter(),get() , etc. Criar o seu próprio gerente permite que você pré-filtro resultados e ter todos esses mesmos métodos, bem como seus próprios métodos personalizados, o trabalho sobre os resultados .

Edição : eu modifiquei meu código para refletir o order_by['?']método. Observe que o gerente retorna um número ilimitado de modelos aleatórios. Por isso, incluí um pouco de código de uso para mostrar como obter apenas um modelo.

from django.db import models

class RandomManager(models.Manager):
    def get_query_set(self):
        return super(RandomManager, self).get_query_set().order_by('?')

class Painting(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)

    objects = models.Manager() # The default manager.
    randoms = RandomManager() # The random-specific manager.

Uso

random_painting = Painting.randoms.all()[0]

Por fim, você pode ter muitos gerentes em seus modelos, portanto, fique à vontade para criar um LeastViewsManager()ou MostPopularManager().

Soviut
fonte
3
O uso de get () só funcionaria se seus pacotes fossem consecutivos, ou seja, você nunca exclui nenhum item. Caso contrário, é provável que você tente obter um pacote que não existe. Usar .all () [random_index] não sofre com esse problema e não é menos eficiente.
21430 Daniel Roseman
Entendi o motivo pelo qual meu exemplo simplesmente replica o código da pergunta com um gerente. Ainda caberá ao OP trabalhar sua verificação de limites.
Soviut
1
em vez de usar .get (id = random_index) não seria melhor usar .filter (id__gte = random_index) [0: 1]? Primeiro, ajuda a resolver o problema com pacotes não consecutivos. Segundo, get_query_set deve retornar ... um QuerySet. E no seu exemplo, isso não acontece.
Nicolas Dumazet 8/06/09
2
Eu não criaria um novo gerente apenas para abrigar um método. Eu adicionaria "get_random" ao gerenciador padrão, para que você não precise passar pelo aro all () [0] toda vez que precisar da imagem aleatória. Além disso, se o autor fosse uma chave estrangeira para um modelo de usuário, você poderia dizer user.painting_set.get_random ().
Antti Rasinen
Normalmente, crio um novo gerente quando quero uma ação geral, como obter uma lista de registros aleatórios. Eu criaria um método no gerenciador padrão se estivesse executando uma tarefa mais específica com os registros que já possuía.
Soviut
6

As outras respostas são potencialmente lentas (usando order_by('?')) ou usam mais de uma consulta SQL. Aqui está um exemplo de solução sem pedido e apenas uma consulta (assumindo o Postgres):

Model.objects.raw('''
    select * from {0} limit 1
    offset floor(random() * (select count(*) from {0}))
'''.format(Model._meta.db_table))[0]

Esteja ciente de que isso gerará um erro de índice se a tabela estiver vazia. Escreva para você uma função auxiliar independente de modelo para verificar isso.

Nelo Mitranim
fonte
Uma boa prova de conceito, mas são duas consultas também dentro do banco de dados, o que você salva é uma ida e volta ao banco de dados. Você teria que executar isso muitas vezes para fazer com que escrever e manter uma consulta bruta valesse a pena. E se você quiser se proteger contra tabelas vazias, é melhor executar count()antecipadamente e dispensar a consulta bruta.
Endre Both
2

Apenas uma ideia simples de como faço:

def _get_random_service(self, professional):
    services = Service.objects.filter(professional=professional)
    i = randint(0, services.count()-1)
    return services[i]
Valter Silva
fonte
1

Apenas para observar um caso especial (bastante comum), se houver uma coluna de incremento automático indexada na tabela sem exclusões, a maneira ideal de fazer uma seleção aleatória é uma consulta como:

SELECT * FROM table WHERE id = RAND() LIMIT 1

que assume essa coluna chamada id para a tabela. No django, você pode fazer isso:

Painting.objects.raw('SELECT * FROM appname_painting WHERE id = RAND() LIMIT 1')

em que você deve substituir appname pelo nome do aplicativo.

Em geral, com uma coluna de identificação, o order_by ('?') Pode ser feito muito mais rapidamente com:

Paiting.objects.raw(
        'SELECT * FROM auth_user WHERE id>=RAND() * (SELECT MAX(id) FROM auth_user) LIMIT %d' 
    % needed_count)
Amir Ali Akbari
fonte
1

Recomenda-se obter uma linha aleatória de um banco de dados relacional

Como usar o django orm para fazer algo assim, o seu servidor db ficará irritado, especialmente se você tiver uma tabela de big data: |

E a solução é fornecer um Model Manager e gravar a consulta SQL manualmente;)

Atualização :

Outra solução que funciona em qualquer back-end de banco de dados, mesmo que não seja rel, sem escrever de forma personalizada ModelManager. Obtendo objetos aleatórios de um Queryset no Django

Alireza Savand
fonte
1

Convém usar a mesma abordagem usada para provar qualquer iterador, especialmente se você planeja experimentar vários itens para criar um conjunto de amostras . @ MatijnPieters e @DzinX pensam muito nisso:

def random_sampling(qs, N=1):
    """Sample any iterable (like a Django QuerySet) to retrieve N random elements

    Arguments:
      qs (iterable): Any iterable (like a Django QuerySet)
      N (int): Number of samples to retrieve at random from the iterable

    References:
      @DZinX:  https://stackoverflow.com/a/12583436/623735
      @MartinPieters: https://stackoverflow.com/a/12581484/623735
    """
    samples = []
    iterator = iter(qs)
    # Get the first `N` elements and put them in your results list to preallocate memory
    try:
        for _ in xrange(N):
            samples.append(iterator.next())
    except StopIteration:
        raise ValueError("N, the number of reuested samples, is larger than the length of the iterable.")
    random.shuffle(samples)  # Randomize your list of N objects
    # Now replace each element by a truly random sample
    for i, v in enumerate(qs, N):
        r = random.randint(0, i)
        if r < N:
            samples[r] = v  # at a decreasing rate, replace random items
    return samples
fogão
fonte
A solução de Matijn e DxinX é para conjuntos de dados que não fornecem acesso aleatório. Para conjuntos de dados que fazem (e SQL faz com OFFSET), isso é desnecessariamente ineficiente.
Endre Both
@EndreBoth de fato. Eu apenas gosto da "eficiência" de codificação de usar a mesma abordagem, independentemente da fonte de dados. Às vezes, a eficiência da amostragem de dados não afeta significativamente o desempenho de um pipeline limitado por outros processos (o que você está realmente fazendo com os dados, como o treinamento em ML).
Placas
1

Uma abordagem muito mais fácil para isso envolve simplesmente filtrar o conjunto de registros de interesse e usar random.samplepara selecionar quantos você quiser:

from myapp.models import MyModel
import random

my_queryset = MyModel.objects.filter(criteria=True)  # Returns a QuerySet
my_object = random.sample(my_queryset, 1)  # get a single random element from my_queryset
my_objects = random.sample(my_queryset, 5)  # get five random elements from my_queryset

Observe que você deve ter algum código para verificar my_querysetse não está vazio; random.sampleretorna ValueError: sample larger than populationse o primeiro argumento contiver muito poucos elementos.

eykanal
fonte
2
Isso fará com que toda a consulta definida seja recuperada?
perrohunter
@perrohunter Nem funciona com Queryset(pelo menos com Python 3.7 e Django 2.1); você deve convertê-lo em uma lista primeiro, o que obviamente recupera todo o conjunto de consultas.
Endre Both
@ EndreBoth - isso foi escrito em 2016, quando nenhum deles existia.
eykanal
Por isso adicionei as informações da versão. Mas se funcionou em 2016, foi o caso, puxando todo o conjunto de consultas para uma lista, certo?
Endre Both
@EndreBoth Correto.
eykanal
1

Oi, eu precisava selecionar um registro aleatório de um conjunto de consultas com o tamanho que eu também precisava informar (por exemplo, uma página da web produziu o item descrito e os registros restantes)

q = Entity.objects.filter(attribute_value='this or that')
item_count = q.count()
random_item = q[random.randomint(1,item_count+1)]

demorou metade do tempo (0,7s vs 1,7s) que:

item_count = q.count()
random_item = random.choice(q)

Suponho que ele evite puxar toda a consulta antes de selecionar a entrada aleatória e torne meu sistema responsivo o suficiente para uma página que é acessada repetidamente para uma tarefa repetitiva em que os usuários desejam ver a contagem de itens_contagem.

pjmnoble
fonte
0

Método para chave primária de incremento automático sem exclusões

Se você possui uma tabela em que a chave primária é um número inteiro seqüencial sem intervalos, o seguinte método deve funcionar:

import random
max_id = MyModel.objects.last().id
random_id = random.randint(0, max_id)
random_obj = MyModel.objects.get(pk=random_id)

Este método é muito mais eficiente do que outros métodos aqui que iteram por todas as linhas da tabela. Embora exija duas consultas ao banco de dados, ambas são triviais. Além disso, é simples e não requer definição de classes extras. No entanto, sua aplicabilidade é limitada a tabelas com uma chave primária de incremento automático, em que as linhas nunca foram excluídas, de modo que não haja lacunas na sequência de IDs.

No caso em que as linhas foram excluídas de forma que sejam lacunas, esse método ainda poderá funcionar se for tentado novamente até que uma chave primária existente seja selecionada aleatoriamente.

Referências

Daniel Himmelstein
fonte
0

Eu tenho uma solução muito simples, faça gerente personalizado:

class RandomManager(models.Manager):
    def random(self):
        return random.choice(self.all())

e depois adicione o modelo:

class Example(models.Model):
    name = models.CharField(max_length=128)
    objects = RandomManager()

Agora, você pode usá-lo:

Example.objects.random()
LagRange
fonte
da escolha aleatória de importação
Adam Starrh 23/04/19
3
Por favor, não use este método, se você quiser velocidade. Esta solução é MUITO lenta. Eu verifiquei. É mais lento que order_by('?').first()mais de 60 vezes.
precisa saber é o seguinte
@ Alex78191 não, "?" também é ruim, mas meu método é EXTRA lento. Eu usei a solução de resposta superior.
LagRange