Najszybszy sposób na uzyskanie pierwszego obiektu z zestawu zapytań w Django?

193

Często mam ochotę pobrać pierwszy obiekt z zestawu zapytań w Django lub zwrócić, Nonejeśli nie ma żadnych. Istnieje wiele sposobów na zrobienie tego, które wszystkie działają. Ale zastanawiam się, który jest najbardziej wydajny.

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

Czy to powoduje dwa wywołania bazy danych? To wydaje się marnotrawstwem. Czy to jest szybsze?

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

Inną opcją byłoby:

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

Generuje to pojedyncze wywołanie bazy danych, co jest dobre. Wymaga to jednak częstego tworzenia obiektu wyjątku, co wymaga dużej ilości pamięci, gdy wszystko, czego naprawdę potrzebujesz, to banalny test wstępny.

Jak mogę to zrobić za pomocą jednego wywołania bazy danych i bez utraty pamięci z obiektami wyjątków?

Leopd
źródło
21
len()Ogólna zasada: jeśli martwisz się minimalizacją zwrotów bazy danych w obie strony, nie używaj zestawu zapytań, zawsze używaj .count().
Daniel DiPaolo,
7
„często tworzy się obiekt wyjątku, co wymaga dużej ilości pamięci” - jeśli martwisz się utworzeniem jednego dodatkowego wyjątku, robisz to źle, ponieważ Python używa wyjątków wszędzie. Czy faktycznie porównałeś, że w twoim przypadku jest to dużo pamięci?
lqc
1
@Leopd A gdybyś rzeczywiście przetestował anwser w jakikolwiek sposób (a przynajmniej komentarze), wiedziałbyś, że nie będzie to szybsze. W rzeczywistości może być wolniejszy, ponieważ tworzysz dodatkową listę, aby ją wyrzucić. A wszystko to tylko orzeszki ziemne w porównaniu z kosztem wywołania funkcji python lub użycia ORM Django w pierwszej kolejności! Pojedyncze wywołanie filter () jest wiele, wiele, wiele razy wolniejsze niż zgłoszenie wyjątku (który wciąż będzie zgłaszany, bo tak działa protokół iteratora!).
lqc
1
Twoja intuicja jest słuszna, że ​​różnica w wydajności jest niewielka, ale twój wniosek jest błędny. Przeprowadziłem test porównawczy, a zaakceptowana odpowiedź jest w rzeczywistości szybsza o realny margines. Domyśl.
Leopd,
11
Dla ludzi korzystających z Django 1.6, oni wreszcie dodał first()i last()wygodę metod: docs.djangoproject.com/en/dev/ref/models/querysets/#first
Wei Yen

Odpowiedzi:

328

Django 1,6 (wydany listopad 2013) wprowadził metody wygodę first() i last()który połknąć otrzymanego wyjątku i powrotu Nonejeżeli nie zwróci queryset obiektów.

cod3monk3y
źródło
1
nie robi [: 1], więc nie jest tak szybki (chyba że i tak musisz ocenić cały zestaw zapytań).
janek37
13
Również first()i last()egzekwowanie ORDER BYklauzuli o zapytaniu. Sprawi, że wyniki będą deterministyczne, ale najprawdopodobniej spowolni zapytanie.
Phil Krylov
@ janek37 nie ma różnic w wydajności. Jak wskazuje cod3monk3y, jest to wygodna metoda i nie odczytuje całego zestawu zapytań.
Zompa,
142

Poprawna odpowiedź to

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

Które mogą być użyte w:

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

Nie chciałbyś najpierw przekształcić go w listę, ponieważ wymusiłoby to pełne wywołanie bazy danych wszystkich rekordów. Po prostu zrób powyższe, a pociągnie tylko pierwszy. Możesz nawet użyć, .order_byaby upewnić się, że otrzymujesz pierwszy, czego chcesz.

Pamiętaj, aby dodać .get()lub otrzymasz QuerySet, a nie obiekt.

szturmowiec
źródło
9
Nadal będziesz musiał go zawinąć przy próbie ... oprócz ObjectDoesNotExist, która jest jak oryginalna trzecia opcja, ale z krojeniem.
Danny W. Adair,
1
Po co ustawiać LIMIT, jeśli w końcu wywołasz get ()? Pozwól ORM i kompilatorowi SQL zdecydować, co jest najlepsze dla jego backendu (na przykład w Oracle Django emuluje LIMIT, więc będzie bolało zamiast pomagać).
lqc
Użyłem tej odpowiedzi bez końcowego .get (). Jeśli lista jest zwracana, zwracam pierwszy element listy.
Keith John Hutchison,
co różni się od posiadania Entry.objects.all()[0]?
James Lin
15
@JamesLin Różnica polega na tym, że [: 1] .get () podnosi DoesNotExist, a [0] podnosi IndexError.
Ropez
49
r = list(qs[:1])
if r:
  return r[0]
return None
Ignacio Vazquez-Abrams
źródło
1
Jeśli włączysz śledzenie, jestem prawie pewien, że zobaczysz ten dodatek LIMIT 1do zapytania i nie wiem, czy możesz zrobić coś lepszego niż to. Jednak wewnętrznie __nonzero__w QuerySetjest zaimplementowane, ponieważ try: iter(self).next() except StopIteration: return false...nie ucieka od wyjątku.
Ben Jackson
@Ben: QuerySet.__nonzero__()nigdy nie jest wywoływany, ponieważ QuerySetjest listsprawdzany przed sprawdzeniem poprawności. Jednak nadal mogą wystąpić inne wyjątki.
Ignacio Vazquez-Abrams
@Aron: Może wygenerować StopIterationwyjątek.
Ignacio Vazquez-Abrams,
konwersja do list === wywołanie, __iter__aby uzyskać nowy obiekt iteratora i wywołać jego nextmetodę doStopIteration wyrzucenia. Tak więc na pewno będzie gdzieś wyjątek;)
lqc
14
Ta odpowiedź jest teraz nieaktualna, spójrz na odpowiedź @ cod3monk3y dla Django 1.6+
1.6+
37

Teraz w Django 1.9 masz first() metodę zestawów zapytań.

YourModel.objects.all().first()

Jest to lepszy sposób niż .get()lub [0]dlatego, że nie zgłasza wyjątku, jeśli zestaw zapytań jest pusty. Therafore, nie trzeba sprawdzać za pomocąexists()

Levi
źródło
1
Powoduje to LIMIT 1 w SQL i widziałem twierdzenia, że ​​może to spowolnić zapytanie - chociaż chciałbym, aby było to uzasadnione: Jeśli zapytanie zwraca tylko jeden element, dlaczego LIMIT 1 naprawdę powinien wpływać na wydajność? Myślę więc, że powyższa odpowiedź jest dobra, ale chciałbym zobaczyć dowody potwierdzające.
rrauenza
Nie powiedziałbym „lepiej”. To naprawdę zależy od twoich oczekiwań.
Trigras
7

Jeśli planujesz często zdobywać pierwszy element - możesz rozszerzyć QuerySet w tym kierunku:

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


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

Zdefiniuj model w następujący sposób:

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

I użyj tego w ten sposób:

 first_object = MyModel.objects.filter(x=100).first()
Nikolay Fominyh
źródło
Wywołaj obiekty = ManagerWithFirstQuery jako obiekty = ManagerWithFirstQuery () - NIE ZAPOMNIJ RODZICÓW - zresztą pomogłeś mi więc +1
Kamil
7

Może to również działać:

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

jeśli jest pusty, to zwraca pustą listę, w przeciwnym razie zwraca pierwszy element na liście.

Nick Cuevas
źródło
1
To zdecydowanie najlepsze rozwiązanie ... skutkuje tylko jednym wywołaniem do bazy danych
Cii
5

Może tak być

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

lub

obj = model.objects.latest('id')
Nauman Tariq
źródło
3

Powinieneś używać metod django, tak jak istnieje. Możesz go użyć.

if qs.exists():
    return qs[0]
return None
Ari
źródło
1
Tyle że, jeśli dobrze to rozumiem, idiomatyczny Python zwykle używa łatwiejszego podejścia do prośby o przebaczenie niż pozwolenie ( EAFP ) niż podejścia typu Look Before You Leap .
BigSmoke
EAFP nie jest tylko zaleceniem stylu, ma powody (na przykład sprawdzenie przed otwarciem pliku nie zapobiega błędom). Tutaj myślę, że istotną kwestią jest to, że istnieje + pobranie elementu powoduje dwa zapytania do bazy danych, które mogą być niepożądane w zależności od projektu i widoku.
Éric Araujo
2

Od wersji django 1.6 możesz używać filter () metodą first () w następujący sposób:

Model.objects.filter(field_name=some_param).first()
dtar
źródło