Агрегация¶
В руководстве по теме API базы данных Django описан способ использования запросов Django для создания, извлечения, обновления и удаления отдельных объектов. Однако иногда вам нужно будет получить значения, полученные путем суммирования или агрегирования коллекции объектов. В этом руководстве описаны способы создания и возврата агрегированных значений с помощью запросов Django.
В этом руководстве мы будем обращаться к следующим моделям. Эти модели используются для отслеживания инвентаря в ряде книжных онлайн-магазинов:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
pubdate = models.DateField()
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
Шпаргалка¶
Спешите? Вот как выполнять общие агрегированные запросы, исходя из моделей выше:
# Total number of books.
>>> Book.objects.count()
2452
# Total number of books with publisher=BaloneyPress
>>> Book.objects.filter(publisher__name="BaloneyPress").count()
73
# Average price across all books, provide default to be returned instead
# of None if no books exist.
>>> from django.db.models import Avg
>>> Book.objects.aggregate(Avg("price", default=0))
{'price__avg': 34.35}
# Max price across all books, provide default to be returned instead of
# None if no books exist.
>>> from django.db.models import Max
>>> Book.objects.aggregate(Max("price", default=0))
{'price__max': Decimal('81.20')}
# Difference between the highest priced book and the average price of all books.
>>> from django.db.models import FloatField
>>> Book.objects.aggregate(
... price_diff=Max("price", output_field=FloatField()) - Avg("price")
... )
{'price_diff': 46.85}
# All the following queries involve traversing the Book<->Publisher
# foreign key relationship backwards.
# Each publisher, each with a count of books as a "num_books" attribute.
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count("book"))
>>> pubs
<QuerySet [<Publisher: BaloneyPress>, <Publisher: SalamiPress>, ...]>
>>> pubs[0].num_books
73
# Each publisher, with a separate count of books with a rating above and below 5
>>> from django.db.models import Q
>>> above_5 = Count("book", filter=Q(book__rating__gt=5))
>>> below_5 = Count("book", filter=Q(book__rating__lte=5))
>>> pubs = Publisher.objects.annotate(below_5=below_5).annotate(above_5=above_5)
>>> pubs[0].above_5
23
>>> pubs[0].below_5
12
# The top 5 publishers, in order by number of books.
>>> pubs = Publisher.objects.annotate(num_books=Count("book")).order_by("-num_books")[:5]
>>> pubs[0].num_books
1323
Создание агрегатов по QuerySet
¶
Django предоставляет два способа генерации агрегатов. Первый способ заключается в генерации суммарных значений по всему QuerySet
. Например, необходимо вычислить среднюю цену всех книг, имеющихся в продаже. Синтаксис запросов Django предоставляет средства для описания множества всех книг:
>>> Book.objects.all()
Нам нужен способ вычисления суммарных значений по объектам, принадлежащим данному QuerySet
. Для этого к QuerySet
добавляется предложение aggregate()
:
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg("price"))
{'price__avg': 34.35}
В данном примере all()
является избыточным, поэтому его можно упростить:
>>> Book.objects.aggregate(Avg("price"))
{'price__avg': 34.35}
Аргумент aggregate()
описывает агрегированное значение, которое мы хотим вычислить - в данном случае среднее значение поля price
в модели Book
. Список доступных агрегатных функций можно найти в Справочник по QuerySet.
aggregate()
является терминальной клаузой для QuerySet
, которая при вызове возвращает словарь пар имя-значение. Имя - это идентификатор значения агрегата, а значение - вычисленный агрегат. Имя автоматически генерируется из имени поля и агрегатной функции. Если необходимо вручную задать имя агрегатного значения, то его можно указать в предложении aggregate:
>>> Book.objects.aggregate(average_price=Avg("price"))
{'average_price': 34.35}
Если необходимо сгенерировать более одного агрегата, то к предложению aggregate()
добавляется еще один аргумент. Так, если бы мы также хотели узнать максимальную и минимальную цену всех книг, мы бы выдали запрос:
>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Avg("price"), Max("price"), Min("price"))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}
Создание агрегатов для каждого элемента в QuerySet
¶
Второй способ создания сводных значений - это создание независимой сводки для каждого объекта в QuerySet
. Например, если вы получаете список книг, вы можете узнать, сколько авторов внесли свой вклад в каждую книгу. Каждая Книга имеет отношение «многие ко многим» с Автором; мы хотим резюмировать эти отношения для каждой книги в QuerySet
.
Сводки по объектам могут быть созданы с помощью предложения annotate()
. Когда указано предложение annotate()
, каждый объект в QuerySet
будет аннотирован указанными значениями.
Синтаксис этих аннотаций идентичен синтаксису, используемому для предложения aggregate()
. Каждый аргумент функции annotate()
описывает агрегат, который должен быть вычислен. Например, чтобы аннотировать книги с указанием количества авторов:
# Build an annotated queryset
>>> from django.db.models import Count
>>> q = Book.objects.annotate(Count("authors"))
# Interrogate the first object in the queryset
>>> q[0]
<Book: The Definitive Guide to Django>
>>> q[0].authors__count
2
# Interrogate the second object in the queryset
>>> q[1]
<Book: Practical Django Projects>
>>> q[1].authors__count
1
Как и в случае с aggregate()
, имя аннотации автоматически выводится из имени агрегатной функции и имени агрегируемого поля. Вы можете отменить это имя по умолчанию, указав псевдоним при задании аннотации:
>>> q = Book.objects.annotate(num_authors=Count("authors"))
>>> q[0].num_authors
2
>>> q[1].num_authors
1
В отличие от aggregate()
, annotate()
не является терминальным предложением. Результатом предложения annotate()
является QuerySet
; этот QuerySet
можно изменить, используя любую другую операцию QuerySet
, включая filter()
, order_by()
или даже дополнительные вызовы annotate()
.
Объединение нескольких агрегатов¶
Объединение нескольких агрегатов с помощью annotate()
выдаст даёт неверные результаты, потому что соединения используются вместо подзапросов:
>>> book = Book.objects.first()
>>> book.authors.count()
2
>>> book.store_set.count()
3
>>> q = Book.objects.annotate(Count("authors"), Count("store"))
>>> q[0].authors__count
6
>>> q[0].store__count
6
Однако для большинства агрегатов избежать этой проблемы невозможно, однако агрегат Count
имеет параметр distinct
, который может помочь:
>>> q = Book.objects.annotate(
... Count("authors", distinct=True), Count("store", distinct=True)
... )
>>> q[0].authors__count
2
>>> q[0].store__count
3
Если сомневаетесь, проверьте SQL-запрос!
Чтобы понять, что происходит в вашем запросе, рассмотрите возможность проверки свойства query
вашего QuerySet
.
Соединения и агрегаты¶
До сих пор мы имели дело с агрегатами по полям, принадлежащим запрашиваемой модели. Однако иногда значение, которое вы хотите агрегировать, будет принадлежать модели, связанной с моделью, которую вы запрашиваете.
При указании поля для агрегирования в агрегатной функции Django позволит вам использовать то же двойное подчеркивание, которое используется при ссылке на связанные поля в фильтрах. Затем Django обработает любые объединения таблиц, необходимые для получения и агрегирования связанного значения.
Например, чтобы найти диапазон цен на книги, предлагаемые в каждом магазине, можно воспользоваться аннотацией:
>>> from django.db.models import Max, Min
>>> Store.objects.annotate(min_price=Min("books__price"), max_price=Max("books__price"))
Это говорит Django получить модель Store
, присоединиться (через отношение многие ко многим) к модели Book
и выполнить агрегирование в поле цены модели книги для получения минимального и максимального значения.
Те же правила применимы и к клаузе aggregate()
. Если бы вы хотели узнать самую низкую и самую высокую цену любой книги, которая имеется в продаже в любом из магазинов, вы могли бы использовать агрегат:
>>> Store.objects.aggregate(min_price=Min("books__price"), max_price=Max("books__price"))
Цепи соединения могут быть настолько глубокими, насколько это необходимо. Например, для получения информации о возрасте самого молодого автора любой книги, имеющейся в продаже, можно задать запрос:
>>> Store.objects.aggregate(youngest_age=Min("books__authors__age"))
Следить за отношениями в обратном направлении¶
Аналогично Поиск, который использует отношения, агрегаты и аннотации к полям моделей или моделей, которые связаны с той, которую вы запрашиваете, могут включать обход «обратных» отношений. Здесь также используются строчные имена связанных моделей и двойные подчеркивания.
Например, мы можем запросить все издательства, аннотированные их соответствующими счетчиками общего книжного запаса (обратите внимание, что мы используем 'book'
для указания обратного внешнего ключа Publisher
-> Book
):
>>> from django.db.models import Avg, Count, Min, Sum
>>> Publisher.objects.annotate(Count("book"))
(Каждый Publisher
в итоговом QuerySet
будет иметь дополнительный атрибут book__count
.)
Мы также можем запросить самую старую книгу из тех, которыми управляет каждое издательство:
>>> Publisher.objects.aggregate(oldest_pubdate=Min("book__pubdate"))
(В полученном словаре будет ключ под названием oldest_pubdate
. Если бы такой псевдоним не был указан, это был бы довольно длинный 'book__pubdate__min'
.)
Это относится не только к внешним ключам. Она также работает с отношениями «многие-ко-многим». Например, мы можем запросить каждого автора, аннотированного общим количеством страниц с учетом всех книг, соавтором которых он является (обратите внимание, что мы используем 'book'
для указания обратного перехода Author
-> Book
от многих ко многим):
>>> Author.objects.annotate(total_pages=Sum("book__pages"))
(Каждый Author
в результирующем QuerySet
будет иметь дополнительный атрибут под названием total_pages
. Если бы такой псевдоним не был указан, это был бы довольно длинный book__pages__sum
.)
Или запросите среднюю оценку всех книг, написанных автором(ами), которые есть у нас в картотеке:
>>> Author.objects.aggregate(average_rating=Avg("book__rating"))
(В итоговом словаре будет ключ под названием 'average_rating'
. Если бы такой псевдоним не был указан, это был бы довольно длинный 'book__rating__avg'
.)
Агрегации и другие предложения QuerySet
¶
filter()
и exclude()
¶
Агрегаты также могут участвовать в фильтрах. Любой filter()
(или exclude()
), применяемый к обычным полям модели, будет иметь эффект ограничения объектов, которые рассматриваются для агрегирования.
При использовании с предложением annotate()
фильтр ограничивает объекты, для которых вычисляется аннотация. Например, с помощью запроса можно сформировать аннотированный список всех книг, название которых начинается с «Django»:
>>> from django.db.models import Avg, Count
>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count("authors"))
При использовании с предложением aggregate()
фильтр ограничивает объекты, по которым вычисляется агрегат. Например, с помощью запроса можно вывести среднюю цену всех книг, название которых начинается с «Django»:
>>> Book.objects.filter(name__startswith="Django").aggregate(Avg("price"))
Фильтрация по аннотациям¶
Аннотированные значения также можно фильтровать. Псевдоним для аннотации можно использовать в предложениях filter()
и exclude()
так же, как и в любом другом поле модели.
Например, для формирования списка книг, имеющих более одного автора, можно выдать запрос:
>>> Book.objects.annotate(num_authors=Count("authors")).filter(num_authors__gt=1)
Этот запрос создает аннотированный набор результатов, а затем создает фильтр на основе этой аннотации.
Если вам нужны две аннотации с двумя отдельными фильтрами, вы можете использовать аргумент filter
с любым агрегатом. Например, для формирования списка авторов с подсчетом книг с высоким рейтингом:
>>> highly_rated = Count("book", filter=Q(book__rating__gte=7))
>>> Author.objects.annotate(num_books=Count("book"), highly_rated_books=highly_rated)
Каждый Author
в наборе результатов будет иметь атрибуты num_books
и highly_rated_books
. См. также Условная агрегация.
Выбор между filter
и QuerySet.filter()
Избегайте использования аргумента filter
с одной аннотацией или агрегированием. Более эффективно использовать QuerySet.filter()
для исключения строк. Аргумент filter
агрегации полезен только при использовании двух или более агрегаций по одним и тем же отношениям с разными условными выражениями.
Порядок предложений annotate()
и filter()
¶
При разработке сложного запроса, который включает предложения annotate()
и filter()
, обратите особое внимание на порядок, в котором эти предложения применяются к QuerySet
.
Когда к запросу применяется предложение annotate()
, аннотация вычисляется по состоянию запроса до точки, где аннотация запрошена. Практическое значение этого заключается в том, что filter()
и annotate()
не являются коммутативными операциями.
Дано:
- У издателя А есть две книги с рейтингом 4 и 5.
- У издателя B есть две книги с рейтингами 1 и 4.
- У издателя C есть одна книга с рейтингом 1.
Приведем пример с агрегатом Count
:
>>> a, b = Publisher.objects.annotate(num_books=Count("book", distinct=True)).filter(
... book__rating__gt=3.0
... )
>>> a, a.num_books
(<Publisher: A>, 2)
>>> b, b.num_books
(<Publisher: B>, 2)
>>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count("book"))
>>> a, a.num_books
(<Publisher: A>, 2)
>>> b, b.num_books
(<Publisher: B>, 1)
Оба запроса возвращают список издателей, у которых есть хотя бы одна книга с рейтингом выше 3,0, поэтому издатель C исключен.
В первом запросе аннотация предшествует фильтру, поэтому фильтр не влияет на аннотацию. distinct=True
требуется, чтобы избежать ошибки запроса.
Второй запрос подсчитывает количество книг с рейтингом выше 3.0 для каждого издателя. Фильтр предшествует аннотации, поэтому фильтр ограничивает объекты, учитываемые при вычислении аннотации.
Приведем еще один пример с агрегатом Avg
:
>>> a, b = Publisher.objects.annotate(avg_rating=Avg("book__rating")).filter(
... book__rating__gt=3.0
... )
>>> a, a.avg_rating
(<Publisher: A>, 4.5) # (5+4)/2
>>> b, b.avg_rating
(<Publisher: B>, 2.5) # (1+4)/2
>>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(
... avg_rating=Avg("book__rating")
... )
>>> a, a.avg_rating
(<Publisher: A>, 4.5) # (5+4)/2
>>> b, b.avg_rating
(<Publisher: B>, 4.0) # 4/1 (book with rating 1 excluded)
Первый запрос запрашивает средний рейтинг всех книг издателя для издателей, у которых есть хотя бы одна книга с рейтингом выше 3.0. Второй запрос запрашивает средний рейтинг книги издателя только для тех оценок, которые превышают 3.0.
Трудно интуитивно понять, как ORM будет переводить сложные наборы запросов в запросы SQL, поэтому в случае сомнений проверьте SQL с помощью str(queryset.query)
и напишите множество тестов.
order_by()
¶
Аннотации можно использовать как основу для сортировки. Когда вы определяете предложение order_by()
, предоставляемые вами агрегаты могут ссылаться на любой псевдоним, определенный как часть предложения annotate()
в запросе.
Например, чтобы упорядочить QuerySet
книг по количеству авторов, принявших участие в создании книги, можно использовать следующий запрос:
>>> Book.objects.annotate(num_authors=Count("authors")).order_by("num_authors")
values()
¶
Обычно аннотации создаются для каждого объекта - аннотированный QuerySet
возвращает один результат для каждого объекта в исходном QuerySet
. Однако, когда предложение values()
используется для ограничения столбцов, возвращаемых в наборе результатов, метод оценки аннотаций немного отличается. Вместо того, чтобы возвращать аннотированный результат для каждого результата в исходном QuerySet
, исходные результаты группируются в соответствии с уникальными комбинациями полей, указанных в предложении values()
. Затем для каждой уникальной группы предоставляется аннотация; аннотация вычисляется по всем членам группы.
Например, рассмотрим запрос автора, который пытается узнать средний рейтинг книг, написанных каждым автором:
>>> Author.objects.annotate(average_rating=Avg("book__rating"))
Это вернет по одному результату для каждого автора в базе данных с аннотацией их среднего книжного рейтинга.
Однако результат будет несколько иным, если использовать клаузулу values()
:
>>> Author.objects.values("name").annotate(average_rating=Avg("book__rating"))
В этом примере авторы будут сгруппированы по именам, поэтому вы получите аннотированный результат только для каждого уникального имени автора. Это означает, что если у вас есть два автора с одним и тем же именем, их результаты будут объединены в один результат в выводе запроса; среднее значение будет вычислено как среднее по книгам, написанным обоими авторами.
Порядок предложений annotate()
и values()
¶
Как и в случае с предложением filter()
, порядок, в котором предложения annotate()
и values()
применяются к запросу, имеет большое значение. Если предложение values()
предшествует annotate()
, аннотация будет вычисляться с использованием группировки, описанной предложением values()
.
Однако, если предложение annotate()
предшествует предложению values()
, аннотации будут сгенерированы по всему набору запросов. В этом случае предложение values()
ограничивает только поля, которые генерируются на выходе.
Например, если поменять местами клаузулы values()
и annotate()
из нашего предыдущего примера:
>>> Author.objects.annotate(average_rating=Avg("book__rating")).values(
... "name", "average_rating"
... )
Теперь для каждого автора будет получен один уникальный результат; однако в выходных данных будет возвращено только имя автора и аннотация average_rating
.
Вы также должны отметить, что average_rating
был явно включен в список возвращаемых значений. Это требуется из-за упорядочения предложений values()
и annotate()
.
Если предложение values()
предшествует предложению annotate()
, любые аннотации будут автоматически добавлены в набор результатов. Однако, если предложение values()
применяется после предложения annotate()
, вам необходимо явно включить агрегированный столбец.
Взаимодействие с order_by()
¶
Поля, упомянутые в части order_by()
кверисета, используются при выборе выходных данных, даже если они не указаны в вызове values()
. Эти дополнительные поля используются для группировки «похожих» результатов вместе, и они могут заставить идентичные строки результатов казаться отдельными. Это проявляется, в частности, при подсчете.
В качестве примера предположим, что у вас есть такая модель:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=10)
data = models.IntegerField()
Если вы хотите подсчитать, сколько раз каждое отдельное значение data
появляется в упорядоченном наборе запросов, вы можете попробовать следующее:
items = Item.objects.order_by("name")
# Warning: not quite correct!
items.values("data").annotate(Count("id"))
…которая сгруппирует объекты Item
по их общим значениям data
, а затем подсчитает количество значений id
в каждой группе. Вот только это не совсем сработает. Упорядочение по name
также будет играть роль в группировке, поэтому этот запрос будет группировать по отдельным парам (data, name)
, а это не то, что вам нужно. Вместо этого следует построить такой набор запросов:
items.values("data").annotate(Count("id")).order_by()
…удаление любой сортировки в запросе. Вы также можете упорядочить, скажем, data
без каких-либо вредных последствий, поскольку они уже играют роль в запросе.
Это поведение такое же, как указано в документации по набору запросов для distinct()
, и общее правило остается тем же: обычно вам не нужны дополнительные столбцы, играющие роль в результате, поэтому очистите сортировку или, по крайней мере, убедитесь, что она ограничен только теми полями, которые вы также выбираете в вызове values()
.
Примечание
Вы можете резонно спросить, почему Django не удаляет за вас лишние столбцы. Основная причина - согласованность с distinct()
и другими местами: Django никогда не удаляет ограничения сортировки, которые вы указали (и мы не можем изменить поведение этих других методов, так как это нарушит нашу политику /misc/api-stable).
Агрегирование аннотаций¶
Вы также можете создать агрегат по результату аннотации. Когда вы определяете предложение aggregate()
, предоставляемые вами агрегаты могут ссылаться на любой псевдоним, определенный как часть предложения annotate()
в запросе.
Например, если необходимо вычислить среднее количество авторов в одной книге, то сначала нужно аннотировать набор книг с количеством авторов, а затем агрегировать это количество авторов, ссылаясь на поле аннотации:
>>> from django.db.models import Avg, Count
>>> Book.objects.annotate(num_authors=Count("authors")).aggregate(Avg("num_authors"))
{'num_authors__avg': 1.66}
Агрегирование по пустым наборам или группам вопросов¶
Когда агрегация применяется к пустому набору или группировке, результат по умолчанию соответствует параметру default, обычно None
. Такое поведение происходит потому, что агрегатные функции возвращают NULL
, когда выполненный запрос не возвращает ни одной строки.
Вы можете указать возвращаемое значение, указав аргумент default для большинства агрегатов. Однако, поскольку Count
не поддерживает аргумент default, он всегда будет возвращать 0
для пустых наборов или групп.
Например, если предположить, что ни одна книга не содержит web в своем названии, вычисление общей цены для этого набора книг вернет None
, поскольку нет совпадающих строк для вычисления агрегирования Sum
:
>>> from django.db.models import Sum
>>> Book.objects.filter(name__contains="web").aggregate(Sum("price"))
{"price__sum": None}
Однако аргумент default может быть задан при вызове Sum
, чтобы вернуть другое значение по умолчанию, если книг не найдено:
>>> Book.objects.filter(name__contains="web").aggregate(Sum("price", default=0))
{"price__sum": Decimal("0")}
Под капотом аргумент default реализуется путем обертывания агрегатной функции в Coalesce
.