Агрегирование
В справочном руководстве по 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. Список доступных агрегатных функций можно найти в Справочнике по 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() предложения в запросе.
Например, чтобы отсортировать набор 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/3.0/topics/db/aggregation/