Агрегирование
В руководстве по 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.all().aggregate(Avg('price'))
{'price__avg': 34.35}
# Max price across all books.
>>> from django.db.models import Max
>>> Book.objects.all().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. Список доступных функций агрегации можно найти в справочнике по наборам запросов.
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() не являются коммутативными операциями.
Предположим:
- Издательство 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()
Устарело начиная с версии 2.2: Начиная с Django 3.1, порядок сортировки по умолчанию модели Meta.ordering не будет использоваться в запросах GROUP BY , таких как .annotate().values(). Начиная с Django 2.2, эти запросы выдают предупреждение об устаревании, указывая на необходимость добавления явного order_by() в набор запросов для отключения предупреждения.
Поля, которые упоминаются в части order_by() набора запросов (или которые используются в порядке сортировки по умолчанию в модели) используются при выборе выходных данных, даже если они не указаны в вызове values() . Эти дополнительные поля используются для группировки похожих результатов, и они могут заставить иначе идентичные строки результатов выглядеть как отдельные. Это особенно заметно при подсчёте вещей.
В качестве примера, предположим, что у вас есть модель такого вида:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=10)
data = models.IntegerField()
class Meta:
ordering = ["name"]
Важная часть здесь — порядок сортировки по умолчанию в поле name . Если вы хотите подсчитать количество раз, когда появляется каждое уникальное значение data , вы можете попробовать это:
# Warning: not quite correct!
Item.objects.values("data").annotate(Count("id"))
…что сгруппирует Item объекты по общим data значениям, а затем подсчитает количество id значений в каждой группе. Но это не сработает. Сортировка по умолчанию по name также будет играть роль в группировке, поэтому этот запрос сгруппирует по уникальным парам (data, name) , что не является желаемым результатом. Вместо этого вы должны создать такой набор запросов:
Item.objects.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/2.2/topics/db/aggregation/