Граф проти лен на наборі запитів Django


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, я б запропонував включати змінну підрахунку під час ітерації (а не за допомогою count) з тих самих причин.


60

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

Дозвольте мені запропонувати вам кілька сценаріїв:

  1. (найважливіше) Коли ви хочете знати лише кількість елементів, і ви не плануєте їх будь-яким способом обробляти, важливо використовувати count():

    DO: queryset.count() - це буде виконувати один SELECT COUNT(*) some_tableзапит, усі обчислення виконуються на стороні RDBMS, 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:


5
Блискуча відповідь, +1 за розміщення QuerySetреалізації контекстуально.
— nehem

4
Цілком буквально ідеальна відповідь. Пояснення, що використовувати і, що більш важливо, чому також.
— Том

28

Думаю, використання тут len(qs)має більше сенсу, оскільки вам потрібно переглядати результати. qs.count()є кращим варіантом, якщо все, що ви хочете зробити, друкує підрахунок, а не перебирає результати.

len(qs)вдарить в базу даних, select * from tableтоді як qs.count()вдарить db за допомогою 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()майже вдвічі швидше, ніж len()у цьому конкретному тестовому випадку.


0

Підсумовуючи те, що вже відповіли інші:

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

Також вірно, що якщо після цієї операції буде повторено весь набір запитів, то в цілому він може бути трохи ефективнішим у використанні len().

Однак

У деяких випадках, наприклад, при обмеженнях пам'яті, може бути зручно (коли це можливо) розділити операцію, виконану на записи. Цього можна досягти за допомогою пагінації django .

Тоді використання count()буде вибором, і ви зможете уникнути необхідності отримувати весь набір запитів одночасно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.