Агрегация¶
В руководстве по теме 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
):
>>> 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.
- У издателя 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 никогда не удаляет ограничения сортировки, которые вы указали (и мы не можем изменить поведение этих других методов, так как это нарушит нашу политику /misc/api-stable).
Агрегирование аннотаций¶
Вы также можете создать агрегат по результату аннотации. Когда вы определяете предложение 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}