Count vs len en un Django QuerySet

93

En Django, dado que tengo un sobre el QuerySetque voy a iterar e imprimir los resultados, ¿cuál es la mejor opción para contar los objetos? len(qs)o qs.count()?

(También dado que contar los objetos en la misma iteración no es una opción).

antonagestam
fuente
2
Interesante pregunta. Sugiero perfilar esto ... ¡Me interesaría mucho! No sé lo suficiente sobre Python para saber si len () en un objeto completamente evaluado tiene mucha sobrecarga. ¡Podría ser más rápido que contar!
Yuji 'Tomita' Tomita

Respuestas:

132

Aunque los documentos de Django recomiendan usar en countlugar de len:

Nota: No utilice len()en QuerySets si todo lo que desea hacer es determinar la cantidad de registros en el conjunto. Es mucho más eficiente manejar un recuento a nivel de la base de datos, usando SQL SELECT COUNT(*), y Django proporciona un count()método precisamente por esta razón.

Dado que está iterando este QuerySet de todos modos, el resultado se almacenará en caché (a menos que lo esté usando iterator), por lo que será preferible usarlo len, ya que esto evita volver a golpear la base de datos, ¡y también la posibilidad de recuperar un número diferente de resultados !) .
Si está utilizando iterator, le sugiero que incluya una variable de recuento a medida que itera (en lugar de utilizar el recuento) por las mismas razones.

Andy Hayden
fuente
60

Elegir entre len()y count()depende de la situación y vale la pena comprender profundamente cómo funcionan para usarlos correctamente.

Permítanme brindarles algunos escenarios:

  1. (lo más importante) Cuando solo desea saber la cantidad de elementos y no planea procesarlos de ninguna manera, es crucial usar count():

    HACER: queryset.count() esto realizará una SELECT COUNT(*) some_tableconsulta única , todo el cálculo se realiza en el lado de RDBMS, Python solo necesita recuperar el número de resultado con un costo fijo de O (1)

    NO HACER: len(queryset) - esto realizará una SELECT * FROM some_tableconsulta, recuperando la tabla completa O (N) y requiriendo memoria O (N) adicional para almacenarla. Esto es lo peor que se puede hacer

  2. Cuando tenga la intención de obtener el conjunto de consultas de todos modos, es un poco mejor usarlo, len()lo que no causará una consulta de base de datos adicional como lo count()haría:

    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
    

    Contar:

    queryset.count() # this will perform an extra db query - len() did not
    
    for obj in queryset: # fetching data
        pass
    
  3. Segundo caso revertido (cuando el conjunto de consultas ya se ha obtenido):

    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)
    

Todo quedará claro una vez que eche un vistazo "debajo del capó":

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)

Buenas referencias en los documentos de Django:

Krzysiek
fuente
5
Brillante respuesta, +1 por publicar la QuerySetimplementación contextualmente.
nehem
4
Literalmente, la respuesta perfecta. Explicar qué usar y, lo que es más importante, el por qué del uso también.
Tom Pegler
28

Creo que el uso len(qs)tiene más sentido aquí, ya que necesita iterar sobre los resultados. qs.count()es una mejor opción si todo lo que quieres hacer es imprimir el recuento y no iterar sobre los resultados.

len(qs)golpeará la base de datos con select * from tablemientras que golpeará la base de datos qs.count()con select count(*) from table.

también qs.count()dará un entero de retorno y no puede iterar sobre él

Rohan
fuente
3

Para las personas que prefieren las mediciones de prueba (Postresql):

Si tenemos un modelo de persona simple y 1000 instancias de él:

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

    def __str__(self):
        return self.name

En caso medio da:

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)

Entonces, ¿cómo puede ver count()casi 2 veces más rápido que len()en este caso de prueba en particular?

funnydman
fuente
0

Resumiendo lo que otros ya han respondido:

  • len() buscará todos los registros e iterará sobre ellos.
  • count() realizará una operación SQL COUNT (mucho más rápido cuando se trata de un gran conjunto de consultas).

También es cierto que si después de esta operación, se iterará todo el conjunto de consultas, entonces, en su conjunto, podría ser un poco más eficiente de usar len().

sin embargo

En algunos casos, por ejemplo cuando hay limitaciones de memoria, puede ser conveniente (cuando sea posible) dividir la operación realizada entre los registros. Eso se puede lograr usando la paginación de django .

Entonces, usar count()sería la elección y podría evitar tener que buscar todo el conjunto de consultas a la vez.

Pablo Guerrero
fuente