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.