Count vs len в Django QuerySet

93

В Django, учитывая, что у меня есть объект, QuerySetкоторый я собираюсь перебрать и распечатать результаты, каков наилучший вариант для подсчета объектов? len(qs)или qs.count()?

(Также учитывая, что подсчет объектов в одной итерации не является вариантом.)

антонагестам
источник
2
Интересный вопрос. Предлагаю профилировать это .. Мне было бы очень интересно! Я недостаточно знаю о Python, чтобы знать, имеет ли len () для полностью оцененных объектов много накладных расходов. Это может быть быстрее, чем считать!
Yuji 'Tomita' Tomita

Ответы:

132

Хотя в документации Django рекомендуется использовать, countа не len:

Примечание. Не используйте len()QuerySets, если все, что вам нужно, - это определить количество записей в наборе. Гораздо эффективнее обрабатывать счетчик на уровне базы данных, используя SQL SELECT COUNT(*), и Django предоставляет count()метод именно для этой цели.

Поскольку вы все равно повторяете этот QuerySet, результат будет кэширован (если вы не используете iterator), и поэтому его будет предпочтительнее использовать len, поскольку это позволяет избежать повторного попадания в базу данных, а также, возможно, получить другое количество результатов !) .
Если вы используете iterator, я бы предложил включить счетную переменную при повторении (вместо использования счетчика) по тем же причинам.

Энди Хайден
источник
60

Выбор между len()и count()зависит от ситуации, и стоит глубоко понять, как они работают, чтобы использовать их правильно.

Позвольте предложить вам несколько сценариев:

  1. (наиболее важно) Если вы хотите знать только количество элементов и не планируете их обрабатывать каким-либо образом, важно использовать count():

    DO: queryset.count() - это будет выполнять один SELECT COUNT(*) some_tableзапрос, все вычисления выполняются на стороне СУБД, Python просто нужно получить номер результата с фиксированной стоимостью O (1)

    НЕ: len(queryset) - это будет выполнять SELECT * FROM some_tableзапрос, извлекая всю таблицу O (N) и требуя дополнительной памяти O (N) для ее хранения. Это худшее, что можно сделать

  2. Когда вы все равно собираетесь получить набор len()запросов, лучше использовать, что не приведет к дополнительному запросу к базе данных, как это count()было бы:

    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
    

    Количество:

    queryset.count() # this will perform an extra db query - len() did not
    
    for obj in queryset: # fetching data
        pass
    
  3. Отмененный 2-й случай (когда набор запросов уже был получен):

    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:

Krzysiek
источник
5
Блестящий ответ, +1 за публикацию QuerySetреализации в контексте.
nehem
4
Буквально идеальный ответ. Объяснение того, что использовать, и, что более важно, причины использования.
Том
28

Я думаю, что использование здесь len(qs)имеет больше смысла, поскольку вам нужно перебирать результаты. qs.count()- лучший вариант, если все, что вы хотите сделать, это распечатать счетчик, а не перебирать результаты.

len(qs)попадет в базу данных с, select * from tableтогда как qs.count()попадет в БД с select count(*) from table.

также qs.count()вернет целое число, и вы не можете перебирать его

Рохан
источник
3

Для людей, предпочитающих тестовые измерения (Postresql):

Если у нас есть простая модель 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()почти в 2 раза быстрее, чем len()в этом конкретном тестовом примере.

шутник
источник
0

Обобщая то, что уже ответили другие:

  • len() будет извлекать все записи и перебирать их.
  • count() выполнит операцию SQL COUNT (намного быстрее при работе с большим набором запросов).

Верно также и то, что если после этой операции будет повторен весь набор запросов, то в целом его можно будет использовать несколько эффективнее len().

Однако

В некоторых случаях, например, при наличии ограничений памяти, может быть удобно (когда возможно) разделить выполняемую операцию по записям. Этого можно добиться с помощью разбивки на страницы django .

Тогда использование count()было бы лучшим выбором, и вы могли бы избежать одновременной выборки всего набора запросов.

Пабло Герреро
источник