Contar vs len em um Django QuerySet

94

No Django, dado que tenho um QuerySetque irei iterar e imprimir os resultados, qual a melhor opção para contar os objetos? len(qs)ou qs.count()?

(Além disso, considerando que contar os objetos na mesma iteração não é uma opção.)

antonagestam
fonte
2
Pergunta interessante. Sugiro traçar um perfil ... Eu ficaria muito interessado! Não sei o suficiente sobre python para saber se len () em objetos totalmente avaliados tem muita sobrecarga. Pode ser mais rápido do que contar!
Yuji 'Tomita' Tomita

Respostas:

133

Embora os documentos do Django recomendem usar em countvez de len:

Nota: Não use len()em QuerySets se tudo o que você deseja fazer é determinar o número de registros no conjunto. É muito mais eficiente lidar com uma contagem no nível do banco de dados, usando SQL SELECT COUNT(*), e o Django fornece um count()método exatamente por esse motivo.

Já que você está iterando este QuerySet de qualquer maneira, o resultado será armazenado em cache (a menos que você esteja usando iterator), e por isso será preferível usar len, pois isso evita atingir o banco de dados novamente e também a possibilidade de recuperar um número diferente de resultados !) .
Se você estiver usando iterator, sugiro incluir uma variável de contagem conforme você itera (em vez de usar a contagem) pelos mesmos motivos.

Andy Hayden
fonte
62

A escolha entre len()e count()depende da situação e vale a pena entender profundamente como funcionam para usá-los corretamente.

Deixe-me apresentar alguns cenários:

  1. (o mais crucial) Quando você deseja apenas saber o número de elementos e não planeja processá-los de forma alguma, é crucial usar count():

    FAZER: queryset.count() - isso executará uma SELECT COUNT(*) some_tableconsulta única , toda a computação é realizada no lado do RDBMS, o Python só precisa recuperar o número do resultado com custo fixo de O (1)

    NÃO FAÇA: len(queryset) - isso executará a SELECT * FROM some_tableconsulta, obtendo toda a tabela O (N) e exigindo memória O (N) adicional para armazená-la. Isso é o pior que pode ser feito

  2. Quando você pretende buscar o queryset de qualquer maneira, é um pouco melhor usar o len()que não causará uma consulta extra ao banco de dados como count()faria:

    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
    

    Contagem:

    queryset.count() # this will perform an extra db query - len() did not
    
    for obj in queryset: # fetching data
        pass
    
  3. 2º caso revertido (quando o queryset já foi buscado):

    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)
    

Tudo ficará claro quando você der uma olhada "sob o 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)

Boas referências na documentação do Django:

Krzysiek
fonte
5
Resposta brilhante, +1 para postar a QuerySetimplementação contextualmente.
nehem
4
Literalmente a resposta perfeita. Explicar o que usar e, mais importante, o porquê do uso também.
Tom Pegler
28

Acho que usar len(qs)faz mais sentido aqui, pois você precisa iterar os resultados. qs.count()é uma opção melhor se tudo o que você deseja fazer imprima a contagem e não itere sobre os resultados.

len(qs)irá atingir o banco de dados com select * from tableenquanto qs.count()irá atingir o banco de dados com select count(*) from table.

também qs.count()fornecerá um inteiro de retorno e você não pode iterar sobre ele

Rohan
fonte
4

Para pessoas que preferem medições de teste (Postresql):

Se tivermos um modelo Person simples e 1000 instâncias dele:

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

    def __str__(self):
        return self.name

No caso médio, dá:

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)

Então, como você pode ver count()quase 2x mais rápido do que len()neste caso de teste específico.

engraçado
fonte
1

Resumindo o que outros já responderam:

  • len() irá buscar todos os registros e iterar sobre eles.
  • count() irá realizar uma operação SQL COUNT (muito mais rápida ao lidar com um grande queryset).

Também é verdade que, se após essa operação, todo o queryset for iterado, então, como todo, ele pode ser um pouco mais eficiente de usar len().

Contudo

Em alguns casos, por exemplo, quando há limitações de memória, pode ser conveniente (quando possível) dividir a operação realizada sobre os registros. Isso pode ser feito usando a paginação django .

Então, usar count()seria a escolha e você poderia evitar ter que buscar o queryset inteiro de uma vez.

Pablo Guerrero
fonte