Il modo più veloce per ottenere il primo oggetto da un queryset in django?


193

Spesso mi ritrovo a voler ottenere il primo oggetto da un queryset in Django, o tornare Nonese non ce ne sono. Ci sono molti modi per farlo che funzionano tutti. Ma mi chiedo quale sia il più performante.

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

Ciò comporta due chiamate al database? Sembra uno spreco. È più veloce?

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

Un'altra opzione sarebbe:

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

Questo genera una singola chiamata al database, il che è positivo. Ma richiede la creazione di un oggetto eccezione per la maggior parte del tempo, il che è una cosa molto dispendiosa in termini di memoria quando tutto ciò di cui hai veramente bisogno è un banale if-test.

Come posso farlo con una sola chiamata al database e senza sfornare la memoria con oggetti eccezione?


21
Regola empirica: se sei preoccupato di ridurre al minimo i round trip di DB, non utilizzare len()sui set di query, utilizza sempre .count().
Daniel DiPaolo,

7
"Creare un oggetto eccezione per la maggior parte del tempo, il che è molto dispendioso in termini di memoria" - se sei preoccupato di creare un'eccezione aggiuntiva, allora stai sbagliando, dato che Python usa eccezioni ovunque. Hai effettivamente verificato che nel tuo caso è ad alta intensità di memoria?
lqc

1
@Leopd E se avessi effettivamente confrontato la risposta in qualche modo (o almeno i commenti), sapresti che non è più veloce. In realtà potrebbe essere più lento, perché stai creando un elenco extra solo per buttarlo fuori. E tutto ciò è solo noccioline rispetto al costo di chiamare una funzione Python o usare l'ORM di Django in primo luogo! Una singola chiamata a filter () è molte, molte, molte volte più lenta quindi sollevando un'eccezione (che verrà comunque sollevata, perché è così che funziona il protocollo iteratore!).
lqc

1
La tua intuizione ha ragione sul fatto che la differenza di prestazioni è piccola, ma la tua conclusione è sbagliata. Ho eseguito un benchmark e la risposta accettata è in effetti più veloce di un margine reale. Vai a capire.
Leopd,

11
Per le persone che utilizzano Django 1.6, hanno finalmente aggiunti i first()e last()convenienza metodi: docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Risposte:


328

Django 1.6 (rilasciato nel novembre 2013) ha introdotto i metodi di convenienza first() e last()che inghiottono l'eccezione risultante e ritornano Nonese il queryset non restituisce oggetti.


2
non fa il [: 1], quindi non è così veloce (a meno che non sia necessario valutare l'intero queryset comunque).
janek37,

13
inoltre, first()e last()applicare una ORDER BYclausola su una query. Renderà deterministici i risultati, ma molto probabilmente rallenterà la query.
Phil Krylov,

@Janek37 non ci sono differenze nelle prestazioni. Come indicato da cod3monk3y, è un metodo conveniente e non legge l'intero queryset.
Zompa,

143

La risposta corretta è

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

Che può essere utilizzato in:

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

Non vorrai prima trasformarlo in un elenco perché ciò costringerebbe una chiamata al database completa di tutti i record. Basta fare quanto sopra e tirerà solo il primo. Puoi persino usarlo .order_byper assicurarti di ottenere il primo che desideri.

Assicurati di aggiungere il .get()altrimenti otterrai un QuerySet e non un oggetto.


9
Dovresti comunque avvolgerlo in un tentativo ... tranne ObjectDoesNotExist, che è come la terza opzione originale ma con lo slicing.
Danny W. Adair il

1
A che serve impostare un LIMIT se alla fine chiamerai get ()? Lascia che l'ORM e il compilatore SQL decidano cosa è meglio per il suo backend (ad esempio, su Oracle Django emula LIMIT, quindi farà male invece di aiutare).
lqc

Ho usato questa risposta senza .get () finale. Se viene restituito un elenco, restituisco il primo elemento dell'elenco.
Keith John Hutchison il

qual è la differenza di avere Entry.objects.all()[0]??
James Lin,

15
@JamesLin La differenza è che [: 1] .get () solleva DoesNotExist, mentre [0] solleva IndexError.
Ropez,

49
r = list(qs[:1])
if r:
  return r[0]
return None

1
Se attivi la traccia, sono abbastanza sicuro che vedrai anche questo aggiungere LIMIT 1alla query e non so che puoi fare di meglio. Tuttavia, internamente __nonzero__in QuerySetè implementato come try: iter(self).next() except StopIteration: return false...quindi non sfugge l'eccezione.
Ben Jackson,

@Ben: QuerySet.__nonzero__()non viene mai chiamato poiché QuerySetviene convertito in a listprima di verificare la veridicità. Tuttavia, possono ancora verificarsi altre eccezioni.
Ignacio Vazquez-Abrams,

@Aron: che può generare StopIterationun'eccezione.
Ignacio Vazquez-Abrams,

convertendo in list === call __iter__per ottenere un nuovo oggetto iteratore e chiamarne il nextmetodo fino a quando non StopIterationviene lanciato. Quindi sicuramente ci sarà un'eccezione da qualche parte;)
lqc

14
Questa risposta è ormai superata, dai un'occhiata a @ cod3monk3y risposta per Django 1.6+
ValAyal,

37

Ora, in Django 1.9 hai un first() metodo per i queryset.

YourModel.objects.all().first()

Questo è un modo migliore di .get()o [0]perché non genera un'eccezione se il queryset è vuoto, pertanto non è necessario verificare utilizzandoexists()


1
Ciò causa un LIMIT 1 in SQL e ho visto affermazioni secondo cui può rallentare la query, anche se mi piacerebbe vederne la motivazione: Se la query restituisce solo un elemento, perché LIMIT 1 dovrebbe davvero influire sulle prestazioni? Quindi penso che la risposta di cui sopra vada bene, ma mi piacerebbe vedere le prove che confermano.
rrauenza,

Non direi "meglio". Dipende davvero dalle tue aspettative.
trigras

7

Se prevedi di ottenere spesso il primo elemento, puoi estendere QuerySet in questa direzione:

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


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

Definire il modello in questo modo:

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

E usalo in questo modo:

 first_object = MyModel.objects.filter(x=100).first()

Chiama oggetti = ManagerWithFirstQuery come objects = ManagerWithFirstQuery () - NON DIMENTICARE I GENITORI - comunque, mi hai aiutato così +1
Kamil,

7

Anche questo potrebbe funzionare:

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

se è vuoto, quindi restituisce un elenco vuoto, altrimenti restituisce il primo elemento all'interno di un elenco.


1
Questa è di gran lunga la migliore soluzione ... si traduce in una sola chiamata al database
Shh

5

Può essere così

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

o

obj = model.objects.latest('id')

3

Dovresti usare i metodi django, come esiste. È lì per te per usarlo.

if qs.exists():
    return qs[0]
return None

1
Tranne il fatto che, se ho capito così bene, Python idiomatico di solito usa un approccio EAFP ( Più facile chiedere perdono che permesso ) piuttosto che un approccio Look Before You Leap .
BigSmoke,

EAFP non è solo una raccomandazione di stile, ha dei motivi (ad esempio, il controllo prima di aprire un file non impedisce errori). Qui penso che la considerazione rilevante sia che esiste + get item causa due query sul database, che possono essere indesiderabili a seconda del progetto e della vista.
Éric Araujo,

2

Da django 1.6 puoi usare filter () con il metodo first () in questo modo:

Model.objects.filter(field_name=some_param).first()
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.