Der schnellste Weg, um das erste Objekt aus einem Abfragesatz in Django zu erhalten?

193

Oft möchte ich das erste Objekt aus einem Abfragesatz in Django holen oder zurückkehren, Nonewenn es keine gibt. Es gibt viele Möglichkeiten, die alle funktionieren. Aber ich frage mich, welches am performantesten ist.

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

Führt dies zu zwei Datenbankaufrufen? Das scheint verschwenderisch. Geht das schneller?

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

Eine andere Option wäre:

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

Dies erzeugt einen einzelnen Datenbankaufruf, was gut ist. Es ist jedoch erforderlich, häufig ein Ausnahmeobjekt zu erstellen. Dies ist sehr speicherintensiv, wenn Sie nur einen trivialen Wenn-Test benötigen.

Wie kann ich dies mit nur einem einzigen Datenbankaufruf tun, ohne den Speicher mit Ausnahmeobjekten zu belasten?

Leopd
quelle
21
Faustregel: Wenn Sie sich len()Gedanken über die Minimierung von DB- Roundtrips machen, verwenden Sie diese nicht für Abfragesätze, sondern immer .count().
Daniel DiPaolo
7
"Oft ein Ausnahmeobjekt erstellen, was sehr speicherintensiv ist" - Wenn Sie Bedenken haben, eine zusätzliche Ausnahme zu erstellen, machen Sie es falsch, da Python überall Ausnahmen verwendet. Haben Sie tatsächlich gemessen, dass es in Ihrem Fall speicherintensiv ist?
lqc
1
@Leopd Und wenn Sie die Antwort tatsächlich in irgendeiner Weise (oder zumindest in den Kommentaren) bewertet hätten, würden Sie wissen, dass sie nicht schneller ist. Es kann tatsächlich langsamer sein, weil Sie eine zusätzliche Liste erstellen, nur um sie wegzuwerfen. Und das alles sind nur Erdnüsse im Vergleich zu den Kosten für den Aufruf einer Python-Funktion oder die Verwendung von Djangos ORM! Ein einzelner Aufruf von filter () ist viele, viele, viele Male langsamer als das Auslösen einer Ausnahme (die immer noch ausgelöst wird, da das Iterator-Protokoll so funktioniert!).
lqc
1
Ihre Intuition ist richtig, dass der Leistungsunterschied gering ist, aber Ihre Schlussfolgerung ist falsch. Ich habe einen Benchmark durchgeführt und die akzeptierte Antwort ist tatsächlich um einiges schneller. Stelle dir das vor.
Leopd
11
Für Leute, die Django 1.6 verwenden, haben sie endlich die first()und last()bequemen Methoden hinzugefügt : docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Antworten:

326

Django 1.6 (veröffentlicht November 2013) eingeführt , um die Bequemlichkeit Methoden first() und last()die schlucken die resultierende Ausnahme und Rückkehr , Nonewenn die queryset keine Objekte zurückgibt.

cod3monk3y
quelle
1
es macht nicht das [: 1], also ist es nicht so schnell (es sei denn, Sie müssen sowieso den gesamten Abfragesatz auswerten).
janek37
13
auch, first()und last()eine Durchsetzung ORDER BYKlausel auf einer Abfrage. Dadurch werden die Ergebnisse deterministisch, die Abfrage wird jedoch höchstwahrscheinlich verlangsamt.
Phil Krylov
@ Janek37 Es gibt keine Unterschiede in der Leistung. Wie durch cod3monk3y angegeben, ist dies eine bequeme Methode und liest nicht den gesamten Abfragesatz.
Zompa
141

Die richtige Antwort ist

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

Welches kann verwendet werden in:

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

Sie möchten es nicht zuerst in eine Liste umwandeln, da dies einen vollständigen Datenbankaufruf aller Datensätze erzwingen würde. Tun Sie einfach das oben genannte und es wird nur das erste ziehen. Sie können sogar .order_bysicherstellen, dass Sie das erste erhalten, das Sie möchten.

.get()Stellen Sie sicher, dass Sie das hinzufügen, sonst erhalten Sie ein QuerySet zurück und kein Objekt.

Sturmheber
quelle
9
Sie müssten es immer noch versuchen ... außer ObjectDoesNotExist, das wie die ursprüngliche dritte Option ist, jedoch mit Slicing.
Danny W. Adair
1
Was bringt es, ein LIMIT festzulegen, wenn Sie am Ende get () aufrufen? Lassen Sie den ORM und den SQL-Compiler entscheiden, was für das Backend am besten ist (z. B. emuliert Oracle Django LIMIT, sodass es weh tut, anstatt zu helfen).
lqc
Ich habe diese Antwort ohne das nachfolgende .get () verwendet. Wenn eine Liste zurückgegeben wird, gebe ich das erste Element der Liste zurück.
Keith John Hutchison
Was ist das Besondere daran Entry.objects.all()[0]?
James Lin
15
@JamesLin Der Unterschied besteht darin, dass [: 1] .get () DoesNotExist auslöst, während [0] IndexError auslöst.
Ropez
49
r = list(qs[:1])
if r:
  return r[0]
return None
Ignacio Vazquez-Abrams
quelle
1
Wenn Sie die Ablaufverfolgung aktivieren, werden Sie sicher sogar sehen, dass dies LIMIT 1der Abfrage hinzugefügt wird , und ich weiß nicht, dass Sie es besser machen können. Intern __nonzero__in QuerySetwird jedoch try: iter(self).next() except StopIteration: return false...so implementiert, dass es der Ausnahme nicht entgeht.
Ben Jackson
@Ben: QuerySet.__nonzero__()wird nie aufgerufen, da das vor der Überprüfung auf Richtigkeit QuerySetin a listkonvertiert wird. Andere Ausnahmen können jedoch weiterhin auftreten.
Ignacio Vazquez-Abrams
@ Aaron: Das kann eine StopIterationAusnahme erzeugen .
Ignacio Vazquez-Abrams
Konvertieren in list === Aufruf __iter__, um ein neues Iteratorobjekt abzurufen und dessen nextMethode aufzurufen , bisStopIteration ausgelöst wird. Also definitiv wird es irgendwo eine Ausnahme geben;)
lqc
14
Diese Antwort ist jetzt veraltet. Schauen Sie sich die Antwort @ cod3monk3y für Django 1.6+ an
ValAyal
37

In Django 1.9 haben Sie jetzt eine first() Methode für Abfragesätze.

YourModel.objects.all().first()

Dies ist ein besserer Weg als .get()oder [0]weil es keine Ausnahme auslöst, wenn das Abfrageset leer ist. Daher müssen Sie die Verwendung nicht überprüfenexists()

levi
quelle
1
Dies führt zu einem LIMIT 1 in SQL, und ich habe Behauptungen gesehen, dass dies die Abfrage verlangsamen kann - obwohl ich dies gerne begründet sehen würde: Wenn die Abfrage nur ein Element zurückgibt, warum sollte LIMIT 1 die Leistung wirklich beeinträchtigen? Daher denke ich, dass die obige Antwort in Ordnung ist, würde aber gerne Beweise sehen, die dies bestätigen.
Rrauenza
Ich würde nicht "besser" sagen. Es hängt wirklich von Ihren Erwartungen ab.
Trigras
7

Wenn Sie vorhaben, das erste Element häufig zu erhalten, können Sie QuerySet in folgende Richtung erweitern:

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


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

Definieren Sie das Modell folgendermaßen:

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

Und benutze es so:

 first_object = MyModel.objects.filter(x=100).first()
Nikolay Fominyh
quelle
Rufen Sie Objekte = ManagerWithFirstQuery als Objekte auf = ManagerWithFirstQuery () - VERGESSEN SIE KEINE ELTERN - trotzdem haben Sie mir so geholfen +1
Kamil
7

Dies könnte auch funktionieren:

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

Wenn es leer ist, wird eine leere Liste zurückgegeben, andernfalls wird das erste Element in einer Liste zurückgegeben.

Nick Cuevas
quelle
1
Dies ist bei weitem die beste Lösung ... führt zu nur einem Aufruf der Datenbank
Shh
5

Es kann so sein

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

oder

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

Sie sollten Django-Methoden verwenden, wie sie existieren. Es ist für Sie da, um es zu benutzen.

if qs.exists():
    return qs[0]
return None
Ari
quelle
1
Wenn ich das nicht richtig verstehe, verwendet idiomatisches Python normalerweise einen EAFP- Ansatz ( Einfacher um Vergebung bitten als Erlaubnis ) anstelle eines Ansatzes " Look Before You Leap" .
BigSmoke
EAFP ist nicht nur eine Stilempfehlung, sondern hat auch Gründe (z. B. verhindert das Überprüfen vor dem Öffnen einer Datei keine Fehler). Hier denke ich, dass die relevante Überlegung ist, dass vorhanden + Element abrufen zwei Datenbankabfragen verursachen, die je nach Projekt und Ansicht unerwünscht sein können.
Éric Araujo
2

Seit django 1.6 können Sie filter () mit der first () -Methode wie folgt verwenden:

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