Count vs len w zestawie zapytań Django

93

W Django, biorąc pod uwagę, QuerySetże mam zamiar powtórzyć i wydrukować wyniki, jaka jest najlepsza opcja do liczenia obiektów? len(qs)czy qs.count()?

(Również biorąc pod uwagę, że liczenie obiektów w tej samej iteracji nie wchodzi w grę).

antonagestam
źródło
2
Interesujące pytanie. Proponuję to sprofilować… Byłbym bardzo zainteresowany! Nie wiem wystarczająco dużo o Pythonie, aby wiedzieć, czy len () na w pełni ocenionych obiektach ma dużo narzutów. To może być szybsze niż liczyć!
Yuji 'Tomita' Tomita

Odpowiedzi:

132

Chociaż dokumentacja Django zaleca używanie countzamiast len:

Uwaga: nie używaj len()w QuerySets, jeśli chcesz tylko określić liczbę rekordów w zestawie. O wiele wydajniej jest obsłużyć liczenie na poziomie bazy danych, używając SQL SELECT COUNT(*), a Django zapewnia count()metodę właśnie z tego powodu.

Ponieważ i tak wykonujesz iterację tego zestawu QuerySet, wynik zostanie zapisany w pamięci podręcznej (chyba że używasz iterator), więc lepiej będzie go użyć len, ponieważ pozwala to uniknąć ponownego uderzenia w bazę danych, a także ewentualnego pobrania innej liczby wyników !) .
Jeśli używasz iterator, sugerowałbym włączenie zmiennej liczącej podczas iteracji (zamiast używania count) z tych samych powodów.

Andy Hayden
źródło
60

Wybór między len()i count()zależy od sytuacji i warto dogłębnie zrozumieć, jak działają, aby prawidłowo z nich korzystać.

Pozwólcie, że przedstawię kilka scenariuszy:

  1. (najważniejsze) Jeśli chcesz znać tylko liczbę elementów i nie planujesz ich w żaden sposób przetwarzać, kluczowe jest użycie count():

    ZRÓB: queryset.count() - to wykona pojedyncze SELECT COUNT(*) some_tablezapytanie, wszystkie obliczenia są wykonywane po stronie RDBMS, Python musi tylko pobrać numer wyniku ze stałym kosztem O (1)

    NIE: len(queryset) - to wykona SELECT * FROM some_tablezapytanie, pobierając całą tabelę O (N) i wymagając dodatkowej pamięci O (N) do jej przechowywania. To najgorsze, co można zrobić

  2. Jeśli mimo wszystko zamierzasz pobrać zestaw zapytań, lepiej jest użyć go, len()który nie spowoduje dodatkowego zapytania do bazy danych, tak jak count():

    len(queryset) # fetching all the data - NO extra cost - data would be fetched anyway in the for loop
    
    for obj in queryset: # data is already fetched by len() - using cache
        pass
    

    Liczyć:

    queryset.count() # this will perform an extra db query - len() did not
    
    for obj in queryset: # fetching data
        pass
    
  3. Cofnięto drugi przypadek (gdy zestaw zapytań został już pobrany):

    for obj in queryset: # iteration fetches the data
        len(queryset) # using already cached data - O(1) no extra cost
        queryset.count() # using cache - O(1) no extra db query
    
    len(queryset) # the same O(1)
    queryset.count() # the same: no query, O(1)
    

Wszystko będzie jasne, gdy spojrzysz „pod maskę”:

class QuerySet(object):

    def __init__(self, model=None, query=None, using=None, hints=None):
        # (...)
        self._result_cache = None

    def __len__(self):
        self._fetch_all()
        return len(self._result_cache)

    def _fetch_all(self):
        if self._result_cache is None:
            self._result_cache = list(self.iterator())
        if self._prefetch_related_lookups and not self._prefetch_done:
            self._prefetch_related_objects()

    def count(self):
        if self._result_cache is not None:
            return len(self._result_cache)

        return self.query.get_count(using=self.db)

Dobre odniesienia w dokumentacji Django:

Krzysiek
źródło
5
Świetna odpowiedź, +1 za umieszczenie QuerySetimplementacji w kontekście.
nehem
4
Dosłownie doskonała odpowiedź. Wyjaśnianie, czego używać i, co ważniejsze, również dlaczego .
Tom Pegler
28

Myślę, że używanie len(qs)ma tutaj więcej sensu, ponieważ musisz iterować wyniki. qs.count()jest lepszą opcją, jeśli wszystko, co chcesz zrobić, drukuje licznik i nie iteruje wyników.

len(qs)trafi do bazy danych z, select * from tablea do bazy danych qs.count()z select count(*) from table.

również qs.count()zwróci liczbę całkowitą i nie możesz po niej iterować

Rohan
źródło
3

Dla osób preferujących pomiary testowe (Postresql):

Jeśli mamy prosty model Person i 1000 jego instancji:

class Person(models.Model):
    name = models.CharField(max_length=100)
    age = models.SmallIntegerField()

    def __str__(self):
        return self.name

W przeciętnym przypadku daje:

In [1]: persons = Person.objects.all()

In [2]: %timeit len(persons)                                                                                                                                                          
325 ns ± 3.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [3]: %timeit persons.count()                                                                                                                                                       
170 ns ± 0.572 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Jak więc możesz widzieć count()prawie 2x szybciej niż len()w tym konkretnym przypadku testowym.

funnydman
źródło
0

Podsumowując, na co inni już odpowiedzieli:

  • len() pobierze wszystkie rekordy i iteruje po nich.
  • count() wykona operację SQL COUNT (znacznie szybciej w przypadku dużego zestawu zapytań).

Prawdą jest również, że jeśli po tej operacji cały zestaw zapytań będzie iterowany, to jako całość może być nieco bardziej efektywny w użyciu len().

jednak

W niektórych przypadkach, na przykład w przypadku ograniczeń pamięci, może być wygodne (o ile to możliwe) podzielenie wykonywanej operacji na rekordy. Można to osiągnąć za pomocą paginacji django .

Wtedy użycie count()byłoby wyborem i można by uniknąć konieczności pobierania całego zestawu zapytań na raz.

Pablo Guerrero
źródło