Count vs len auf einem Django QuerySet

89

QuerySetWas ist in Django die beste Option zum Zählen der Objekte , da ich eine habe , über die ich iterieren und die Ergebnisse drucken werde? len(qs)oder qs.count()?

(Auch da das Zählen der Objekte in derselben Iteration keine Option ist.)

antonagestam
quelle
2
Interessante Frage. Ich schlage vor, dies zu profilieren. Ich wäre sehr interessiert! Ich weiß nicht genug über Python, um zu wissen, ob len () für vollständig evaluierte Objekte viel Overhead hat. Es könnte schneller sein als zählen!
Yuji 'Tomita' Tomita

Antworten:

129

Obwohl die Django-Dokumente die Verwendung empfehlen, countanstatt len:

Hinweis: Verwenden Sie diese len()Option nicht für QuerySets, wenn Sie lediglich die Anzahl der Datensätze im Set bestimmen möchten. Es ist viel effizienter, eine Zählung auf Datenbankebene mithilfe von SQL durchzuführen SELECT COUNT(*), und Django bietet count()aus genau diesem Grund eine Methode.

Da Sie dieses QuerySet ohnehin iterieren, wird das Ergebnis zwischengespeichert (sofern Sie es nicht verwenden iterator). Daher ist es vorzuziehen, es zu verwenden len, da dies verhindert, dass die Datenbank erneut aufgerufen wird und möglicherweise auch eine andere Anzahl von Ergebnissen abgerufen wird !) .
Wenn Sie verwenden iterator, würde ich aus den gleichen Gründen vorschlagen, eine Zählvariable einzuschließen, während Sie durchlaufen (anstatt count zu verwenden).

Andy Hayden
quelle
56

Die Wahl zwischen len()und count()hängt von der jeweiligen Situation ab. Es lohnt sich, genau zu verstehen, wie sie funktionieren, um sie richtig zu verwenden.

Lassen Sie mich einige Szenarien vorstellen:

  1. (am wichtigsten) Wenn Sie nur die Anzahl der Elemente wissen möchten und nicht vorhaben, sie in irgendeiner Weise zu verarbeiten, ist es wichtig, Folgendes zu verwenden count():

    DO: queryset.count() - Dies führt eine einzelne SELECT COUNT(*) some_tableAbfrage durch. Alle Berechnungen werden auf der RDBMS-Seite ausgeführt. Python muss nur die Ergebnisnummer mit den festen Kosten von O (1) abrufen.

    NICHT: len(queryset) - Dies führt eine SELECT * FROM some_tableAbfrage durch, ruft die gesamte Tabelle O (N) ab und benötigt zusätzlichen O (N) -Speicher zum Speichern. Dies ist das Schlimmste, was getan werden kann

  2. Wenn Sie das Abfrageset trotzdem abrufen möchten, ist es etwas besser zu verwenden len(), da dies keine zusätzliche Datenbankabfrage verursachtcount() dies wäre:

    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

    Anzahl:

    queryset.count() # this will perform an extra db query - len() did not
    
    for obj in queryset: # fetching data
        pass
  3. 2. Fall rückgängig gemacht (wenn der Abfragesatz bereits abgerufen wurde):

    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)

Alles wird klar, wenn Sie einen Blick "unter die Haube" werfen:

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)

Gute Referenzen in Django-Dokumenten:

Krzysiek
quelle
5
Geniale Antwort, +1 für die QuerySetkontextbezogene Veröffentlichung der Implementierung.
Nehem
4
Im wahrsten Sinne des Wortes die perfekte Antwort. Erklären, was zu verwenden ist und, was noch wichtiger ist, warum auch.
Tom Pegler
27

Ich denke, die Verwendung len(qs)ist hier sinnvoller, da Sie die Ergebnisse durchlaufen müssen.qs.count()ist eine bessere Option, wenn alles, was Sie tun möchten, die Anzahl druckt und nicht über die Ergebnisse iteriert.

len(qs)wird die Datenbank mit treffen, select * from tablewährend die Datenbank mit qs.count()treffen wirdselect count(*) from table .

Außerdem qs.count()wird return integer angegeben, und Sie können nicht darüber iterieren

Rohan
quelle
3

Für Personen, die Testmessungen bevorzugen (Postresql):

Wenn wir ein einfaches Personenmodell und 1000 Instanzen davon haben:

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

    def __str__(self):
        return self.name

Im Durchschnitt gibt es:

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)

Wie können Sie also count()fast zweimal schneller sehen als len()in diesem speziellen Testfall?

Funnydman
quelle
0

Zusammenfassen, was andere bereits beantwortet haben:

  • len() ruft alle Datensätze ab und iteriert darüber.
  • count() führt eine SQL COUNT-Operation aus (viel schneller bei großen Abfragesätzen).

Es ist auch richtig, dass, wenn nach dieser Operation der gesamte Abfragesatz iteriert wird, die Verwendung insgesamt etwas effizienter sein könnte len().

jedoch

In einigen Fällen, beispielsweise bei Speicherbeschränkungen, kann es zweckmäßig sein (wenn möglich), die ausgeführte Operation auf die Datensätze aufzuteilen. Dies kann durch Django-Paginierung erreicht werden .

Dann count()wäre die Verwendung die Wahl, und Sie könnten vermeiden, den gesamten Abfragesatz auf einmal abrufen zu müssen.

Pablo Guerrero
quelle