¿Cómo extraer un registro aleatorio usando el ORM de Django?

176

Tengo un modelo que representa pinturas que presento en mi sitio. En la página web principal me gustaría mostrar algunos de ellos: el más nuevo, uno que no fue visitado por la mayoría del tiempo, el más popular y uno aleatorio.

Estoy usando Django 1.0.2.

Si bien los primeros 3 de ellos son fáciles de extraer utilizando modelos django, el último (aleatorio) me causa algunos problemas. Desde mi punto de vista puedo codificarlo en algo como esto:

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

No parece algo que me gustaría tener en mi opinión, esto es completamente parte de la abstracción de la base de datos y debería estar en el modelo. Además, aquí necesito cuidar los registros eliminados (entonces el número de todos los registros no me cubrirá todos los valores clave posibles) y probablemente muchas otras cosas.

¿Alguna otra opción para hacerlo, preferiblemente de alguna manera dentro de la abstracción del modelo?

kender
fuente
En mi opinión, la forma en que muestra las cosas y las cosas que muestra es parte del nivel de "Vista" o la lógica empresarial que debería ir en el nivel de "Controlador" de MVC.
Gabriele D'Antona
En Django, el controlador es la vista. docs.djangoproject.com/en/dev/faq/general/…

Respuestas:

169

El uso order_by('?')matará al servidor db el segundo día de producción. Una mejor manera es algo como lo que se describe en Obtener una fila aleatoria de una base de datos 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
fuente
45
¿Cuáles son los beneficios de model.objects.aggregate(count=Count('id'))['count']más?model.objects.all().count()
Ryan Saxe
11
Si bien es mucho mejor que la respuesta aceptada, tenga en cuenta que este enfoque realiza dos consultas SQL. Si el recuento cambia en el medio, podría ser posible obtener un error fuera de los límites.
Nelo Mitranim
2
Esta es una solución incorrecta. No funcionará si sus identificadores no comienzan desde 0. Y también cuando los identificadores no son contiguos. Digamos, el primer registro comienza desde 500 y el último es 599 (suponiendo contigüidad). Entonces el recuento sería 54950. Seguramente la lista [54950] no existe porque la longitud de su consulta es 100. Lanzará el índice fuera de la excepción enlazada. No sé por qué tantas personas votaron por esto y esto fue marcado como respuesta aceptada.
dijo el
1
@sajid: ¿Por qué, exactamente, me preguntas? Es bastante fácil ver la suma total de mis contribuciones a esta pregunta: editar un enlace para apuntar a un archivo después de que se pudrió. Ni siquiera he votado sobre ninguna de las respuestas. Pero me parece divertido que esta respuesta y la que usted dice que es mucho mejor, ambas, .all()[randint(0, count - 1)]en efecto. Tal vez debería concentrarse en identificar qué parte de la respuesta es incorrecta o débil, en lugar de redefinir "por error" para nosotros y gritar a los votantes tontos. (¿Tal vez es que no está usando .objects?)
Nathan Tuggy
3
@NathanTuggy. Ok mi mal Lo siento
sajid
260

Simplemente use:

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

Está documentado en la API de QuerySet .

muhuk
fuente
71
Tenga en cuenta que este enfoque puede ser muy lento, como se documenta :)
Nicolas Dumazet
66
"puede ser costoso y lento, dependiendo del backend de la base de datos que esté utilizando". - alguna experiencia en diferentes backends DB? (sqlite / mysql / postgres)?
kender
44
No lo he probado, así que esto es pura especulación: ¿por qué debería ser más lento que recuperar todos los elementos y realizar la aleatorización en Python?
muhuk
8
Leí que es lento en mysql, ya que mysql tiene un orden aleatorio increíblemente ineficiente.
Brandon Henry el
33
¿Por qué no solo random.choice(Model.objects.all())?
Jamey
25

Las soluciones con order_by ('?') [: N] son ​​extremadamente lentas incluso para tablas medianas si usa MySQL (no sabe sobre otras bases de datos).

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

Significa que para cada fila de la tabla se ejecutará la función RAND (), luego se clasificará toda la tabla de acuerdo con el valor de esta función y luego se devolverán los primeros N registros. Si sus mesas son pequeñas, está bien. Pero en la mayoría de los casos, esta es una consulta muy lenta.

Escribí una función simple que funciona incluso si los ID tienen agujeros (algunas filas se eliminaron):

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]

Es más rápido que order_by ('?') En casi todos los casos.

Mikhail Korobov
fuente
30
Además, lamentablemente, está lejos de ser aleatorio. Si tiene un registro con id 1 y otro con id 100, le devolverá el segundo el 99% del tiempo.
DS.
16

Aquí hay una solución simple:

from random import randint

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

Puede crear un administrador en su modelo para hacer este tipo de cosas. Para entender primero qué es un gerente es, el Painting.objectsmétodo es un gerente que contiene all(), filter(), get(), etc. La creación de su propio gestor le permite comprobar la validez de los resultados de filtro y tienen todos estos mismos métodos, así como sus propios métodos personalizados, el trabajo sobre los resultados .

EDITAR : modifiqué mi código para reflejar el order_by['?']método. Tenga en cuenta que el administrador devuelve un número ilimitado de modelos aleatorios. Debido a esto, he incluido un poco de código de uso para mostrar cómo obtener un solo 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 último, puede tener muchos gerentes en sus modelos, así que siéntase libre de crear un LeastViewsManager()o MostPopularManager().

Soviut
fuente
3
Usar get () solo funcionaría si tus pks son consecutivos, es decir, nunca eliminas ningún elemento. De lo contrario, es probable que intente obtener un paquete que no existe. El uso de .all () [random_index] no sufre este problema y no es menos eficiente.
Daniel Roseman
Entendí que por eso mi ejemplo simplemente replica el código de la pregunta con un gerente. Dependerá de la OP para resolver sus controles de límites.
Soviut
1
en lugar de usar .get (id = random_index), ¿no sería mejor usar .filter (id__gte = random_index) [0: 1]? Primero, ayuda a resolver el problema con pks no consecutivos. En segundo lugar, get_query_set debería devolver ... un QuerySet. Y en tu ejemplo, no lo hace.
Nicolas Dumazet
2
No crearía un nuevo gerente solo para albergar un método. Agregaría "get_random" al administrador predeterminado para que no tenga que pasar por el aro all () [0] cada vez que necesite la imagen aleatoria. Además, si el autor fuera una clave foránea para un modelo de usuario, podría decir user.painting_set.get_random ().
Antti Rasinen
Normalmente creo un nuevo administrador cuando quiero una acción general, como obtener una lista de registros aleatorios. Crearía un método en el administrador predeterminado si estuviera haciendo una tarea más específica con los registros que ya tenía.
Soviut
6

Las otras respuestas son potencialmente lentas (usando order_by('?')) o usan más de una consulta SQL. Aquí hay una solución de muestra sin ordenar y solo una consulta (suponiendo Postgres):

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

Tenga en cuenta que esto generará un error de índice si la tabla está vacía. Escríbete una función auxiliar agnóstica de modelo para verificar eso.

Nelo Mitranim
fuente
Una buena prueba de concepto, pero también son dos consultas dentro de la base de datos, lo que guarda es un viaje de ida y vuelta a la base de datos. Tendría que ejecutar esto muchas veces para que valga la pena escribir y mantener una consulta sin formato. Y si desea protegerse contra las tablas vacías, también podría ejecutar un count()avance y prescindir de la consulta sin procesar.
Endre Both
2

Solo una simple idea de cómo lo hago:

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

Solo para notar un caso especial (bastante común), si hay una columna de incremento automático indexada en la tabla sin eliminaciones, la forma óptima de hacer una selección aleatoria es una consulta como:

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

eso supone una columna llamada id para la tabla. En django puedes hacer esto:

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

en el que debe reemplazar appname con el nombre de su aplicación.

En general, con una columna de identificación, order_by ('?') Se puede hacer mucho más rápido con:

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

Esto es muy recomendable Obtener una fila aleatoria de una base de datos relacional

Debido a que usar django orm para hacer algo así, su servidor db se enojará especialmente si tiene una tabla de datos grandes: |

Y la solución es proporcionar un administrador de modelos y escribir la consulta SQL a mano;)

Actualización :

Otra solución que funciona en cualquier backend de base de datos, incluso los que no son rel sin escribir de forma personalizada ModelManager. Obtener objetos aleatorios de un conjunto de consultas en Django

Alireza Savand
fuente
1

Es posible que desee utilizar el mismo enfoque que usaría para muestrear cualquier iterador, especialmente si planea muestrear múltiples elementos para crear un conjunto de muestra . @MatijnPieters y @DzinX piensan mucho en esto:

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
encimeras
fuente
La solución de Matijn y DxinX es para conjuntos de datos que no proporcionan acceso aleatorio. Para los conjuntos de datos que sí lo hacen (y SQL sí OFFSET), esto es innecesariamente ineficiente.
Endre Both
@EndreBoth de hecho. Simplemente me gusta la "eficiencia" de codificación de usar el mismo enfoque independientemente de la fuente de datos. A veces, la eficiencia del muestreo de datos no afecta significativamente el rendimiento de una tubería limitada por otros procesos (lo que sea que esté haciendo realmente con los datos, como el entrenamiento de ML).
Hobs
1

Un enfoque mucho más fácil para esto implica simplemente filtrar hasta el conjunto de registros de interés y usar random.samplepara seleccionar tantos como desee:

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

Tenga en cuenta que debe tener algún código para verificar que my_querysetno esté vacío; random.sampledevuelve ValueError: sample larger than populationsi el primer argumento contiene muy pocos elementos.

eykanal
fuente
2
¿Esto hará que se recupere todo el conjunto de consultas?
perrohunter
@perrohunter Ni siquiera funcionará con Queryset(al menos con Python 3.7 y Django 2.1); primero debe convertirlo en una lista, que obviamente recupera todo el conjunto de consultas.
Endre Both
@EndreBoth: esto se escribió en 2016, cuando ninguno de los dos existía.
eykanal
Es por eso que agregué la información de la versión. Pero si funcionó en 2016, lo hizo al incluir todo el conjunto de consultas en una lista, ¿verdad?
Endre Both
@EndreBoth Correcto.
eykanal
1

Hola, necesitaba seleccionar un registro aleatorio de un conjunto de consultas cuya longitud también necesitaba informar (es decir, la página web produjo el elemento descrito y dichos registros quedaron)

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

tomó la mitad de tiempo (0.7s vs 1.7s) que:

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

Supongo que evita desplegar toda la consulta antes de seleccionar la entrada aleatoria e hizo que mi sistema responda lo suficiente para una página a la que se accede repetidamente para una tarea repetitiva en la que los usuarios desean ver la cuenta regresiva de item_count.

pjmnoble
fuente
0

Método para incrementar automáticamente la clave primaria sin eliminaciones

Si tiene una tabla donde la clave primaria es un entero secuencial sin espacios, entonces el siguiente método debería 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 es mucho más eficiente que otros métodos aquí que iteran a través de todas las filas de la tabla. Si bien requiere dos consultas a la base de datos, ambas son triviales. Además, es simple y no requiere definir ninguna clase adicional. Sin embargo, su aplicabilidad se limita a las tablas con una clave primaria de incremento automático donde las filas nunca se han eliminado, de modo que no hay espacios en la secuencia de identificadores.

En el caso de que se hayan eliminado filas que son espacios, este método aún podría funcionar si se vuelve a intentar hasta que se seleccione aleatoriamente una clave primaria existente.

Referencias

Daniel Himmelstein
fuente
0

Obtuve una solución muy simple, haga un administrador personalizado:

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

y luego agregue el modelo:

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

Ahora puedes usarlo:

Example.objects.random()
LagRange
fuente
de elección aleatoria de importación
Adam Starrh
3
Por favor, no use este método, si quiere velocidad. Esta solución es MUY lenta. He comprobado. Es más lento que order_by('?').first()más de 60 veces.
LagRange
@ Alex78191 no, "?" también es malo, pero mi método es EXTRA lento. Usé la solución de respuesta superior.
LagRange