Spec-Zone.ru › Django 5.0

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

В руководстве по 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. Список доступных функций агрегирования можно найти в Справочнике по наборам запросов.

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() не являются коммутативными операциями.

Предположим:

  • Издатель 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}

Агрегирование на пустых наборах запросов или группах

Когда агрегация применяется к пустому набору запросов или группе, результат по умолчанию принимает значение своего аргумента 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.0/topics/db/aggregation/

Spec-Zone.ru

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