Граф проти лен на наборі запитів 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.