Агрегирование
В руководстве по 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. Это делается путем добавления клаузы aggregate() к QuerySet:
>>> 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, которая при вызове возвращает словарь пар имя-значение. Имя — идентификатор агрегированного значения; значение — вычисленное агрегированное значение. Имя автоматически генерируется из имени поля и агрегатной функции. Если вы хотите вручную указать имя агрегированного значения, вы можете сделать это, указав это имя при указании клаузы агрегирования:
>>> 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.
- Издательство Б имеет две книги с оценками 1 и 4.
- Издательство В имеет одну книгу с оценкой 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, поэтому издательство В исключается.
В первом запросе аннотация предшествует фильтру, поэтому фильтр не влияет на аннотацию. 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() в запросе.
Например, чтобы отсортировать список книг по количеству авторов, внесших вклад в книгу, можно использовать следующий запрос:
>>> 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 никогда не удаляет ограничения сортировки, которые вы указали (и мы не можем изменить поведение других методов, так как это нарушило бы нашу политику стабильности API).
Агрегирование аннотаций
Вы также можете генерировать агрегат на основе результата аннотации. При определении условия 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.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.2/topics/db/aggregation/