Spec-Zone.ru › Django 4.2

Агрегирование

В руководстве по 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.
>>> from django.db.models import Avg
>>> Book.objects.aggregate(Avg("price"))
{'price__avg': 34.35}

# Max price across all books.
>>> from django.db.models import Max
>>> Book.objects.aggregate(Max("price"))
{'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 обратного ключа foreign key):

>>> 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 полезен только при использовании двух или более агрегатов над одними и теми же отношениями с разными условиями.

END_OF_DOCUMENT_MARKER

Порядок применения annotate() и filter()

При разработке сложных запросов, включающих как annotate(), так и filter() условия, обратите особое внимание на порядок их применения к QuerySet.

Когда к запросу применяется annotate() условие, аннотация вычисляется на основе состояния запроса до момента запроса аннотации. Практическим следствием этого является то, что filter() и annotate() не являются коммутативными операциями.

Допустим:

  • Издательство A имеет две книги с рейтингами 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 никогда не удаляет ограничения сортировки, которые вы задали (и мы не можем изменить поведение других методов, так как это нарушит нашу политику стабильности 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}

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.2/topics/db/aggregation/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API