Spec-Zone.ru › Django 2.1

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

В руководстве по 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('books', filter=Q(books__rating__gte=7))
>>> Author.objects.annotate(num_books=Count('books'), highly_rated_books=highly_rated)

Каждый Author в наборе результатов будет иметь атрибуты num_books и highly_rated_books.

Выбор между filter и QuerySet.filter()

Избегайте использования аргумента filter с одной аннотацией или агрегацией. Более эффективно использовать QuerySet.filter() для исключения строк. Аргумент агрегации filter полезен только при использовании двух или более агрегаций по одним и тем же отношениям с разными условиями.

Изменено в Django 2.0:

К агрегатам был добавлен аргумент 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()

    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.1/topics/db/aggregation/

Spec-Zone.ru

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