¿La forma más rápida de obtener el primer objeto de un conjunto de consultas en django?

193

A menudo me encuentro con ganas de obtener el primer objeto de un conjunto de consultas en Django, o regresar Nonesi no hay ninguno. Hay muchas maneras de hacer esto que funcionan. Pero me pregunto cuál es el más eficaz.

qs = MyModel.objects.filter(blah = blah)
if qs.count() > 0:
    return qs[0]
else:
    return None

¿Esto resulta en dos llamadas a la base de datos? Eso parece un desperdicio. ¿Es esto más rápido?

qs = MyModel.objects.filter(blah = blah)
if len(qs) > 0:
    return qs[0]
else:
    return None

Otra opción sería:

qs = MyModel.objects.filter(blah = blah)
try:
    return qs[0]
except IndexError:
    return None

Esto genera una sola llamada a la base de datos, lo cual es bueno. Pero requiere crear un objeto de excepción la mayor parte del tiempo, lo cual es una tarea que requiere mucha memoria cuando todo lo que realmente necesita es una prueba trivial if.

¿Cómo puedo hacer esto con una sola llamada a la base de datos y sin agitar la memoria con objetos de excepción?

Leopd
fuente
21
Regla de oro: si le preocupa minimizar los viajes de ida y vuelta de la base de datos, no lo use len()en conjuntos de consultas, use siempre .count().
Daniel DiPaolo
77
"crear un objeto de excepción la mayor parte del tiempo, lo que requiere mucha memoria": si le preocupa crear una excepción adicional, lo está haciendo mal, ya que Python usa excepciones por todas partes. ¿Realmente evaluaste que tu memoria requiere mucha memoria?
lqc
1
@Leopd Y si en realidad hubieras comparado la respuesta de alguna manera (o al menos los comentarios), sabrías que no es más rápido. En realidad, puede ser más lento, porque estás creando una lista adicional solo para descartarla. ¡Y todo eso es solo maní en comparación con el costo de llamar a una función de Python o usar el ORM de Django en primer lugar! Una sola llamada a filter () es muchas, muchas, muchas veces más lenta que generar una excepción (que aún se generará, ¡porque así es como funciona el protocolo iterador!).
lqc
1
Su intuición es correcta de que la diferencia de rendimiento es pequeña, pero su conclusión es incorrecta. Ejecuté un punto de referencia y la respuesta aceptada es de hecho más rápida por un margen real. Imagínate.
Leopd
11
Para las personas que utilizan Django 1.6, finalmente han añadido las first()y last()conveniencia métodos: docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Respuestas:

328

Django 1.6 (lanzado en noviembre de 2013) introdujo los métodos de conveniencia first() y se last()traga la excepción resultante y regresa Nonesi el conjunto de consultas no devuelve objetos.

cod3monk3y
fuente
1
no hace el [: 1], por lo que no es tan rápido (a menos que necesite evaluar todo el conjunto de consultas de todos modos).
janek37
13
también, first()y last()hacer cumplir una ORDER BYcláusula en una consulta. Hará que los resultados sean deterministas, pero probablemente disminuirá la velocidad de la consulta.
Phil Krylov
@ janek37 no hay diferencias en el rendimiento. Como lo indica cod3monk3y, es un método conveniente y no lee todo el conjunto de consultas.
Zompa
142

La respuesta correcta es

Entry.objects.all()[:1].get()

Que se puede usar en:

Entry.objects.filter()[:1].get()

No querrás convertirlo primero en una lista porque eso forzaría una llamada a la base de datos completa de todos los registros. Simplemente haga lo anterior y solo extraerá el primero. Incluso podría usar .order_bypara asegurarse de obtener lo primero que desea.

Asegúrese de agregar .get()o de lo contrario obtendrá un QuerySet y no un objeto.

Stormlifter
fuente
9
Aún necesitaría envolverlo en un intento ... excepto ObjectDoesNotExist, que es como la tercera opción original pero con corte.
Danny W. Adair
1
¿Cuál es el punto de establecer un LÍMITE si vas a llamar a get () al final? Deje que el ORM y el compilador de SQL decidan qué es lo mejor para su backend (por ejemplo, en Oracle Django emula LIMIT, por lo que perjudicará en lugar de ayudar).
lqc
Usé esta respuesta sin el .get () final. Si se devuelve una lista, devuelvo el primer elemento de la lista.
Keith John Hutchison
¿Cuál es la diferencia de tener Entry.objects.all()[0]?
James Lin
15
@JamesLin La diferencia es que [: 1] .get () aumenta DoesNotExist, mientras que [0] aumenta IndexError.
Ropez
49
r = list(qs[:1])
if r:
  return r[0]
return None
Ignacio Vazquez-Abrams
fuente
1
Si activa el rastreo, estoy bastante seguro de que incluso verá este complemento LIMIT 1a la consulta, y no sé si puede hacer algo mejor que esto. Sin embargo, a nivel interno __nonzero__en el QuerySetque se implementa como try: iter(self).next() except StopIteration: return false...para que no se escapen de la excepción.
Ben Jackson
@Ben: QuerySet.__nonzero__()nunca se llama ya que QuerySetse convierte en a listantes de verificar la veracidad. Sin embargo, aún pueden ocurrir otras excepciones.
Ignacio Vazquez-Abrams
@Aron: Eso puede generar una StopIterationexcepción.
Ignacio Vazquez-Abrams
convertir a list === call __iter__para obtener un nuevo objeto iterador y llamar a su nextmétodo hasta que StopIterationse lance. Así que definitivamente habrá una excepción en alguna parte;)
lqc
14
Esta respuesta ahora está desactualizada, eche un vistazo a la respuesta @ cod3monk3y para Django 1.6+
ValAyal
37

Ahora, en Django 1.9 tienes un first() método para conjuntos de consultas.

YourModel.objects.all().first()

Esta es una mejor manera que .get()o [0]porque no arroja una excepción si el conjunto de consultas está vacío. Por lo tanto, no es necesario verificar usandoexists()

levi
fuente
1
Esto causa un LÍMITE 1 en el SQL y he visto afirmaciones de que puede hacer que la consulta sea más lenta, aunque me gustaría ver eso confirmado: si la consulta solo devuelve un elemento, ¿por qué el LÍMITE 1 realmente debería afectar el rendimiento? Así que creo que la respuesta anterior está bien, pero me encantaría ver la confirmación de la evidencia.
rrauenza
Yo no diría "mejor". Realmente depende de tus expectativas.
trigras
7

Si planea obtener el primer elemento con frecuencia, puede extender QuerySet en esta dirección:

class FirstQuerySet(models.query.QuerySet):
    def first(self):
        return self[0]


class ManagerWithFirstQuery(models.Manager):
    def get_query_set(self):
        return FirstQuerySet(self.model)

Defina un modelo como este:

class MyModel(models.Model):
    objects = ManagerWithFirstQuery()

Y úsalo así:

 first_object = MyModel.objects.filter(x=100).first()
Nikolay Fominyh
fuente
Llamar a objetos = ManagerWithFirstQuery como objetos = ManagerWithFirstQuery () - NO OLVIDES A LOS PADRES - de todos modos, me ayudaste +1
Kamil
7

Esto podría funcionar también:

def get_first_element(MyModel):
    my_query = MyModel.objects.all()
    return my_query[:1]

si está vacío, devuelve una lista vacía, de lo contrario, devuelve el primer elemento dentro de una lista.

Nick Cuevas
fuente
1
Esta es, de lejos, la mejor solución ... resulta en una sola llamada a la base de datos
Shh
5

Puede ser asi

obj = model.objects.filter(id=emp_id)[0]

o

obj = model.objects.latest('id')
Nauman Tariq
fuente
3

Debe usar métodos django, como existe. Está ahí para que lo uses.

if qs.exists():
    return qs[0]
return None
Ari
fuente
1
Excepto, si lo entiendo correctamente, Python idiomático generalmente usa un enfoque más fácil de pedir perdón que permiso ( EAFP ) en lugar de un enfoque de mirar antes de saltar .
BigSmoke
EAFP no es solo una recomendación de estilo, tiene razones (por ejemplo, verificar antes de abrir un archivo no evita errores). Aquí creo que la consideración relevante es que existe + obtener elemento causa dos consultas de base de datos, que pueden ser indeseables dependiendo del proyecto y la vista.
Éric Araujo
2

Desde django 1.6 puede usar filter () con el método first () de esta manera:

Model.objects.filter(field_name=some_param).first()
dtar
fuente