Count vs len sur un Django QuerySet


93

Dans Django, étant donné que j'ai un sur QuerySetlequel je vais parcourir et imprimer les résultats, quelle est la meilleure option pour compter les objets? len(qs)ou qs.count()?

(Aussi étant donné que compter les objets dans la même itération n'est pas une option.)


2
Question interessante. Je suggère de profiler ceci .. Je serais très intéressé! Je n'en connais pas assez sur python pour savoir si len () sur un objet entièrement évalué a beaucoup de surcharge. Cela pourrait être plus rapide que compter!
Yuji 'Tomita' Tomita le

Réponses:


132

Bien que la documentation Django recommande d'utiliser countplutôt que len:

Remarque: ne pas utiliser len()sur les QuerySets si tout ce que vous voulez faire est de déterminer le nombre d'enregistrements dans l'ensemble. Il est beaucoup plus efficace de gérer un décompte au niveau de la base de données, en utilisant SQL SELECT COUNT(*), et Django fournit une count()méthode précisément pour cette raison.

Puisque vous itérez de toute façon ce QuerySet, le résultat sera mis en cache (sauf si vous l'utilisez iterator), et il sera donc préférable de l'utiliser len, car cela évite de frapper à nouveau la base de données, et aussi la possibilité de récupérer un nombre différent de résultats !) .
Si vous utilisez iterator, alors je suggérerais d'inclure une variable de comptage au fur et à mesure que vous parcourez (plutôt que d'utiliser count) pour les mêmes raisons.


60

Le choix entre len()et count()dépend de la situation et il vaut la peine de comprendre profondément comment ils fonctionnent pour les utiliser correctement.

Laissez-moi vous présenter quelques scénarios:

  1. (le plus crucial) Lorsque vous souhaitez uniquement connaître le nombre d'éléments et que vous ne prévoyez pas de les traiter de quelque manière que ce soit, il est essentiel d'utiliser count():

    ACTION: queryset.count() - cela effectuera une seule SELECT COUNT(*) some_tablerequête, tous les calculs sont effectués côté SGBDR, Python a juste besoin de récupérer le numéro de résultat avec un coût fixe de O (1)

    NE PAS: len(queryset) - cela effectuera une SELECT * FROM some_tablerequête, récupérant toute la table O (N) et nécessitant de la mémoire O (N) supplémentaire pour la stocker. C'est le pire qui puisse être fait

  2. Lorsque vous avez l'intention de récupérer le jeu de requêtes de toute façon, il est légèrement préférable de l'utiliser, len()ce qui ne provoquera pas une requête de base de données supplémentaire comme le count()ferait:

    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
    

    Compter:

    queryset.count() # this will perform an extra db query - len() did not
    
    for obj in queryset: # fetching data
        pass
    
  3. 2ème cas annulé (lorsque le jeu de requêtes a déjà été récupéré):

    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)
    

Tout sera clair une fois que vous aurez jeté un coup d'œil "sous le capot":

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)

Bonnes références dans la documentation Django:


5
Réponse brillante, +1 pour la publication de l' QuerySetimplémentation contextuelle.
nehem

4
Littéralement la réponse parfaite. Expliquer ce qu'il faut utiliser et, plus important encore, le pourquoi de l'utilisation.
Tom Pegler

28

Je pense que l'utilisation len(qs)est plus logique ici car vous devez parcourir les résultats. qs.count()est une meilleure option si tout ce que vous voulez faire imprimer le décompte et ne pas parcourir les résultats.

len(qs)frappera la base de données avec select * from tablealors que qs.count()frappera la base de données avec select count(*) from table.

qs.count()donnera également un entier de retour et vous ne pourrez pas le parcourir


3

Pour les personnes qui préfèrent les mesures de test (Postresql):

Si nous avons un modèle Person simple et 1000 instances de celui-ci:

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

    def __str__(self):
        return self.name

En moyenne, cela donne:

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)

Alors, comment pouvez-vous voir count()presque 2 fois plus vite que len()dans ce cas de test particulier.


0

Résumant ce que les autres ont déjà répondu:

  • len() va chercher tous les enregistrements et les parcourir.
  • count() effectuera une opération SQL COUNT (beaucoup plus rapide en cas de gros jeu de requêtes).

Il est également vrai que si après cette opération, l'ensemble du jeu de requêtes sera itéré, alors dans son ensemble, il pourrait être légèrement plus efficace à utiliser len().

pourtant

Dans certains cas, par exemple lorsque la mémoire est limitée, il peut être pratique (si possible) de fractionner l'opération effectuée sur les enregistrements. Cela peut être réalisé en utilisant la pagination django .

Ensuite, utiliser count()serait le choix et vous pourriez éviter d'avoir à récupérer l'ensemble de la requête en une seule fois.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.