在Django QuerySet中,Count()与len()有何区别?

128
在Django中,如果我有一个要迭代并打印结果的QuerySet,那么计算对象数量的最佳选项是什么? len(qs) 还是 qs.count()
(也假设在相同迭代中计算对象数量不是一种选择。)

2
有趣的问题。我建议对此进行分析。我会非常感兴趣!我不太了解Python,不知道在完全评估的对象上使用len()是否有很大的开销。这可能比count()更快! - Yuji 'Tomita' Tomita
6个回答

168
尽管Django文档推荐使用count而不是len:
请注意:如果您只想确定集合中的记录数,请勿在QuerySet上使用len()。更高效的方法是在数据库层面处理计数,使用SQL的SELECT COUNT(*),Django提供了count()方法正是为此而提供的。
既然您已经在迭代此QuerySet,结果将被缓存(除非您使用iterator),因此最好使用len,因为这避免了再次访问数据库,以及可能检索到不同数量的结果的可能性! 如果使用iterator,那么出于相同的原因,我建议在迭代时包括一个计数变量(而不是使用计数).

148
在选择使用len()count()之间,取决于具体情况。深入理解它们的工作原理,才能正确地使用它们。

我给你提供一些场景:

  1. (最关键的)当您只想知道元素数量,并且不打算以任何方式处理它们时,使用count()非常重要:

DO: queryset.count() - 这将执行单个SELECT COUNT(*) FROM some_table查询,所有计算都在RDBMS端执行,Python只需要检索结果数字,固定成本为O(1)

DON'T: len(queryset) - 这将执行SELECT * FROM some_table查询,获取整个表O(N),并且需要额外的O(N)内存来存储它。这是最差的情况

  1. 如果您打算获取查询集,稍微好一点的方法是使用len(),因为它不会像count()一样导致额外的数据库查询

len()(一个数据库查询)

    len(queryset) # SELECT * 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

count()(两个数据库查询!):

    queryset.count() # First db query SELECT COUNT(*)

    for obj in queryset: # Second db query (fetching data) SELECT *
        pass
  1. Reverted 2nd case (when queryset has already been fetched):

     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)
    

只要你深入了解“内部结构”,一切就会变得清晰明了:

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)

在Django文档中有很好的参考资料:


6
很棒的回答,因为你在上下文中发布了“QuerySet”实现的代码,所以加1分。 - nehem
4
确切地说,这是一个完美的答案。它解释了什么应该使用,更重要的是解释了为什么要使用它。 - Thomas Pegler

29

我认为在这里使用 len(qs) 更有意义,因为您需要遍历结果。qs.count() 是一个更好的选择,如果您只想打印计数,而不是遍历结果。

len(qs) 将使用 select * from table 命令对数据库进行查询,而 qs.count() 则会使用 select count(*) from table 命令。

此外,qs.count() 将返回整数,您无法对其进行迭代。


6

针对喜欢测试测量(Postgresql)的人:

如果我们有一个简单的Person模型,以及1000个实例:

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

    def __str__(self):
        return self.name

在平均情况下,它会得到:

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)

那么在这个特定的测试案例中,你如何看到count()len()快近2倍


3

总结其他人已经回答的内容:

  • len() 将获取所有记录并对它们进行迭代。
  • count() 将执行 SQL COUNT 操作(处理大型查询集时速度更快)。

另外,如果在这个操作后,整个查询集将被迭代,那么作为整体来说使用 len() 可能会稍微更有效率。

然而

在某些情况下,例如具有内存限制时,将操作分割成多次可以更加方便(如果可能的话)。 这可以通过使用Django分页来实现。

然后,使用 count() 就是选择,你可以避免一次性获取整个查询集。


1

我进行了一个实验,测试了使用count()len()函数在10百万行数据中查询速度的差异。*我使用了PostgreSQL数据库。

<实验结果>

count() len()
模型查询 1.02秒 46.13秒
原始查询 0.48秒 3.16秒

因此,速度最快的顺序如下:

  1. count()与原始查询 (0.48秒)
  2. count()与模型查询 (1.02秒)
  3. len()与原始查询 (3.16秒)
  4. len()与模型查询 (46.13秒)

我建议基本上使用count()与模型查询,因为它比len()更快,代码更少,更方便,而且比原始查询更方便。但是,当使用select_for_update()时,您应该使用len()与模型查询,因为select_for_update()count()不起作用,而且比原始查询更方便。

<如何实验>

首先,我创建了只有idTest模型

# "store/models.py"

from django.db import models

class Test(models.Model):
    pass

然后,运行以下命令:

python manage.py makemigrations && python manage.py migrate

然后,使用psql一次性向store_test插入了1000万行

postgres=# INSERT INTO store_test (id) SELECT generate_series(1, 10000000);
INSERT 0 10000000
Time: 29929.337 ms (00:29.929)

最后,我按照下面所示运行了test_view()函数:
# "store/views.py"

from time import time
from .models import Test
from django.db import connection
from django.http import HttpResponse

def test_view(request):

    # "count()" with model query
    start = time()
    print(Test.objects.all().count(), "- count() - Model query")
    end = time()
    print(round(end - start, 2), "seconds\n")

    # "len()" with model query
    start = time()
    print(len(Test.objects.all()), "- len() - Model query")
    end = time()
    print(round(end - start, 2), "seconds\n")
    
    # "count()" with raw query
    start = time()
    with connection.cursor() as cursor:
        cursor.execute("SELECT count(*) FROM store_test;") 
        print(cursor.fetchone()[0], "- count() - Raw query")
    end = time()
    print(round(end - start, 2), "seconds\n")

    # "len()" with raw query
    start = time()
    with connection.cursor() as cursor:
        cursor.execute("SELECT * FROM store_test;")
        print(len(cursor.fetchall()), "- len() - Raw query") 
    end = time()
    print(round(end - start, 2), "seconds\n")
    
    return HttpResponse("Test_view")

在控制台上输出:

10000000 - count() - Model query
1.02 seconds

10000000 - len() - Model query
46.13 seconds

10000000 - count() - Raw query
0.48 seconds

10000000 - len() - Raw query
3.16 seconds

[18/Dec/2022 07:12:14] "GET /store/test_view/ HTTP/1.1" 200 9

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接