Agregación

La guía sobre el tema API de abstracción de base de datos de Django describe la forma en que puedes utilizar consultas de Django para crear, recuperar, actualizar y eliminar objetos individuales. Sin embargo, a veces necesitarás recuperar valores derivados al resumir o agregar una colección de objetos. Esta guía sobre el tema describe las formas en que se pueden generar y devolver valores agregados utilizando consultas de Django.

A lo largo de esta guía, nos referiremos a los siguientes modelos. Estos modelos se utilizan para rastrear la inventario de una serie de librerías en línea:

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)

Hoja de referencia rápida

En apuros? Aquí tienes cómo realizar consultas agregadas comunes, asumiendo los modelos anteriores:

# 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

Generar agregados sobre un QuerySet

Django proporciona dos formas de generar agregados. La primera forma es generar valores resumen sobre un conjunto de objetos completo QuerySet. Por ejemplo, supongamos que deseas calcular el precio promedio de todos los libros disponibles para la venta. El sintaxis de consulta de Django proporciona una forma de describir el conjunto de todos los libros:

>>> Book.objects.all()

Lo que necesitamos es una forma de calcular valores resumen sobre los objetos que pertenecen a este QuerySet. Esto se hace agregando una cláusula aggregate() al QuerySet:

>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg("price"))
{'price__avg': 34.35}

La traducción de los textos es la siguiente:

>>> Book.objects.aggregate(Avg("price"))
{'price__avg': 34.35}

El argumento del cláusula aggregate() describe el valor agregado que queremos computar - en este caso, la media del campo price del modelo Book. Una lista de las funciones agregadas disponibles se puede encontrar en la referencia de QuerySet.

aggregate() es una cláusula terminal para un QuerySet que, cuando se invoca, devuelve un diccionario de pares nombre-valor. El nombre es un identificador para el valor agregado; el valor es el valor agregado computado. El nombre se genera automáticamente a partir del nombre del campo y la función agregada. Si quieres especificar manualmente un nombre para el valor agregado, puedes hacerlo proporcionando ese nombre cuando especifiques la cláusula de agregación:

>>> Book.objects.aggregate(average_price=Avg("price"))
{'average_price': 34.35}

Si deseas generar más de un agregado, debes agregar otro argumento a la cláusula aggregate(). Por lo tanto, si también queremos saber el máximo y mínimo precio de todos los libros, emitiríamos la consulta:

>>> 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')}

Generar agregados para cada elemento en un QuerySet

La segunda forma de generar valores resumen es generar un resumen independiente para cada objeto en un QuerySet. Por ejemplo, si estás recuperando una lista de libros, puede que quieras saber cuántos autores contribuyeron a cada libro. Cada Libro tiene una relación muchos-a-muchos con el Autor; queremos resumir esta relación para cada libro en el QuerySet.

Los resúmenes por objeto se pueden generar utilizando la annotate() cláusula. Cuando se especifica una cláusula de anotación, cada objeto en el QuerySet será anotado con los valores especificados.

La sintaxis para estas anotaciones es idéntica a la utilizada por la aggregate() cláusula. Cada argumento a annotate() describe un agregado que se debe calcular. Por ejemplo, para anotar libros con el número de autores:

# 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

Al igual que aggregate(), el nombre para la anotación se deriva automáticamente del nombre de la función agregada y el nombre del campo que se está agrupando. Puedes sobreescribir este nombre por defecto proporcionando un alias cuando especifiques la anotación:

>>> q = Book.objects.annotate(num_authors=Count("authors"))
>>> q[0].num_authors
2
>>> q[1].num_authors
1

A diferencia de aggregate(), annotate() no es una cláusula terminal. La salida de la cláusula annotate() es un QuerySet; este QuerySet puede ser modificado utilizando cualquier otra operación de QuerySet, incluyendo filter(), order_by() o incluso llamadas adicionales a annotate().

Combina varias agregaciones

Combina varias agregaciones con annotate() producirá resultados incorrectos porque se utilizan joins en lugar de subconsultas:

>>> 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

Para la mayoría de las agregaciones, no hay forma de evitar este problema, sin embargo, el agregado Count tiene un parámetro distinct que puede ayudar:

>>> q = Book.objects.annotate(
...     Count("authors", distinct=True), Count("store", distinct=True)
... )
>>> q[0].authors__count
2
>>> q[0].store__count
3

Si tienes dudas, inspecciona la consulta SQL!

Para entender qué sucede en tu consulta, considera inspeccionar la propiedad query de tu conjunto de consultas.

Uniones y agregados

Hasta ahora, hemos tratado con agregaciones sobre campos que pertenecen al modelo que se está consultando. Sin embargo, a veces el valor que deseas agrupar pertenecerá a un modelo relacionado con el modelo que estás consultando.

Cuando especificas el campo a ser agregado en una función de agregación, Django permitirá utilizar la misma notación de doble subrayado que se utiliza cuando se hace referencia a campos relacionados en los filtros. Django entonces manejará cualquier unión de tablas que sean necesarias para recuperar y agrupar el valor relacionado.

Por ejemplo, para encontrar la gama de precios de libros ofrecidos en cada tienda, podrías utilizar la anotación:

>>> from django.db.models import Max, Min
>>> Store.objects.annotate(min_price=Min("books__price"), max_price=Max("books__price"))

Esto le dice a Django que recupere el modelo Store, realice una unión (a través de la relación muchos-a-muchos) con el modelo Book y agrupe sobre el campo de precio del modelo libro para producir un valor mínimo y máximo.

Los textos traducidos son:

>>> Store.objects.aggregate(min_price=Min("books__price"), max_price=Max("books__price"))

Las cadenas de unión pueden ser tan profundas como requieras. Por ejemplo, para extraer la edad del autor más joven de cualquier libro disponible para la venta, podrías emitir la consulta:

>>> Store.objects.aggregate(youngest_age=Min("books__authors__age"))

Seguir relaciones en sentido contrario

De manera similar a Lookups que abarcan relaciones, las agregaciones y anotaciones en campos de modelos o modelos relacionados con el que estás consultando pueden incluir recorrer «relaciones en sentido contrario». Aquí se utilizan también los nombres de modelos en minúsculas y dobles guiones bajos.

Por ejemplo, podemos preguntar por todos los editores, anotados con sus respectivos contadores totales de stock de libros (nota cómo usamos 'book' para especificar el salto de clave foránea en sentido contrario Publisher -> Book):

>>> from django.db.models import Avg, Count, Min, Sum
>>> Publisher.objects.annotate(Count("book"))

(Cada Publisher en la QuerySet resultante tendrá un atributo adicional llamado book__count.)

También podemos preguntar por el libro más antiguo de cualquiera de los gestionados por cada editor:

>>> Publisher.objects.aggregate(oldest_pubdate=Min("book__pubdate"))

(El diccionario resultante tendrá una clave llamada 'oldest_pubdate'. Si no se especificara tal alias, sería el bastante largo 'book__pubdate__min'.)

Esto no se aplica solo a las claves foráneas. También funciona con muchas a muchas relaciones. Por ejemplo, podemos preguntar por cada autor, anotado con el número total de páginas considerando todos los libros que el autor (co-)autoricó (nota cómo usamos 'book' para especificar el salto de muchas a muchas en sentido contrario Author -> Book):

>>> Author.objects.annotate(total_pages=Sum("book__pages"))

(Cada Author en la QuerySet resultante tendrá un atributo adicional llamado total_pages. Si no se especificara tal alias, sería el bastante largo book__pages__sum.)

Preguntar por la calificación promedio de todos los libros escritos por autor(es) que tenemos en archivo:

>>> Author.objects.aggregate(average_rating=Avg("book__rating"))

(El diccionario resultante tendrá una clave llamada 'average_rating'. Si no se hubiera especificado un alias, sería el bastante largo 'book__rating__avg'.)

Agregaciones y otras cláusulas de consulta de QuerySet

filter() y exclude()

Las agregaciones también pueden participar en filtros. Cualquier filtro (filter() o exclude()) aplicado a campos de modelo normales tendrá el efecto de restringir los objetos que se consideran para la agregación.

Al utilizarse con una cláusula annotate(), un filtro tiene el efecto de restringir los objetos para los cuales se calcula una anotación. Por ejemplo, puedes generar una lista anotada de todos los libros que tienen un título que comienza con «Django» utilizando la consulta:

>>> from django.db.models import Avg, Count
>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count("authors"))

Al utilizarse con una cláusula aggregate(), un filtro tiene el efecto de restringir los objetos sobre los cuales se calcula la agregación. Por ejemplo, puedes generar el precio promedio de todos los libros que tienen un título que comienza con «Django» utilizando la consulta:

>>> Book.objects.filter(name__startswith="Django").aggregate(Avg("price"))

Filtrado en anotaciones

Los valores anotados también pueden ser filtrados. El alias para la anotación se puede utilizar en cláusulas filter() y exclude() de la misma manera que cualquier otro campo de modelo.

Por ejemplo, para generar una lista de libros que tienen más de un autor, puedes emitir la consulta:

>>> Book.objects.annotate(num_authors=Count("authors")).filter(num_authors__gt=1)

Esta consulta genera un conjunto de resultados anotados, y luego genera una filtro basado en esa anotación.

Si necesitas dos anotaciones con dos filtros separados puedes usar el argumento filter con cualquier agregado. Por ejemplo, para generar una lista de autores con un recuento de libros altamente valorados:

>>> highly_rated = Count("book", filter=Q(book__rating__gte=7))
>>> Author.objects.annotate(num_books=Count("book"), highly_rated_books=highly_rated)

Cada autor en el conjunto de resultados tendrá los atributos num_books y highly_rated_books. Consulta también Condición de agregación condicional.

Elegir entre filter y QuerySet.filter()

Evita usar el argumento filter con una sola anotación o agregado. Es más eficiente usar QuerySet.filter() para excluir filas. El argumento de agregado filter solo es útil cuando se usan dos o más agregados sobre las mismas relaciones con condicionales diferentes.

Orden de las cláusulas annotate() y filter()

Cuando estés desarrollando una consulta compleja que involucre tanto la cláusula annotate() como la cláusula filter(), presta mucha atención al orden en el que se aplican las cláusulas a QuerySet.

Cuando se aplica una cláusula annotate() a una consulta, la anotación se calcula sobre el estado de la consulta hasta el punto donde se solicita la anotación. La implicación práctica de esto es que filter() y annotate() no son operaciones comutativas.

Dado:

  • Editor A tiene dos libros con calificaciones 4 y 5.

  • Los textos traducidos son:

  • El editor C tiene un libro con una calificación de 1.

Aquí tienes un ejemplo con el agregado 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)

Ambas consultas devuelven una lista de editores que tienen al menos un libro con una calificación superior a 3.0, por lo tanto el editor C está excluido.

En la primera consulta, la anotación precede al filtro, por lo tanto el filtro no tiene efecto en la anotación. Es necesario establecer distinct=True para evitar un bug de consulta.

La segunda consulta cuenta el número de libros que tienen una calificación superior a 3.0 para cada editor. El filtro precede a la anotación, por lo tanto el filtro limita los objetos considerados al calcular la anotación.

Aquí tienes otro ejemplo con el agregado 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)

La primera consulta pide la calificación promedio de todos los libros de un editor para editores que tienen al menos un libro con una calificación superior a 3.0. La segunda consulta pide la media de las calificaciones de los libros de un editor, pero solo considera aquellas calificaciones superiores a 3.0.

Es difícil intuir cómo el ORM traducirá conjuntos de consultas complejos en consultas SQL, por lo tanto cuando tienes dudas, inspecciona la consulta SQL con str(queryset.query) y escribe muchas pruebas.

order_by()

Los textos traducidos son:

Por ejemplo, para ordenar un conjunto de libros por el número de autores que han contribuido al libro, podrías usar la siguiente consulta:

>>> Book.objects.annotate(num_authors=Count("authors")).order_by("num_authors")

values()

Ordinariamente, las anotaciones se generan en una base por objeto - un conjunto de consultas anotadas devolverá un resultado para cada objeto en el conjunto original de consultas. Sin embargo, cuando se utiliza una cláusula values() para limitar las columnas que se devuelven en el conjunto de resultados, el método para evaluar anotaciones es ligeramente diferente. En lugar de devolver un resultado anotado para cada resultado en el conjunto original de consultas, los resultados originales se agrupan según las combinaciones únicas de campos especificados en la cláusula values(). Una anotación se proporciona entonces para cada grupo único; la anotación se calcula sobre todos los miembros del grupo.

Por ejemplo, considere una consulta de autor que intenta encontrar el promedio de calificaciones de libros escritos por cada autor:

>>> Author.objects.annotate(average_rating=Avg("book__rating"))

Esto devolverá un resultado para cada autor en la base de datos, anotado con su promedio de calificación de libro.

Sin embargo, el resultado será ligeramente diferente si se utiliza una cláusula values():

>>> Author.objects.values("name").annotate(average_rating=Avg("book__rating"))

En este ejemplo, los autores se agruparán por nombre, por lo que solo obtendrás un resultado anotado para cada nombre de autor único. Esto significa que si tienes dos autores con el mismo nombre, sus resultados se fusionarán en un solo resultado en la salida de la consulta; el promedio se calculará como el promedio sobre los libros escritos por ambos autores.

Orden de las cláusulas annotate() y values()

Como con la cláusula filter(), el orden en que las cláusulas annotate() y values() se aplican a una consulta es significativo. Si la cláusula values() precede a la cláusula annotate(), la anotación se calculará utilizando el agrupamiento descrito por la cláusula values().

Sin embargo, si la cláusula annotate() precede a la cláusula values(), las anotaciones se generarán sobre todo el conjunto de consultas. En este caso, la cláusula values() solo restringe los campos que se generan en la salida.

Por ejemplo, si invertimos el orden de las cláusulas values() y annotate() de nuestro ejemplo anterior:

>>> Author.objects.annotate(average_rating=Avg("book__rating")).values(
...     "name", "average_rating"
... )

Esto producirá un resultado único para cada autor; sin embargo, solo se devolverán el nombre del autor y la anotación average_rating en los datos de salida.

También debes tener en cuenta que average_rating ha sido incluido explícitamente en la lista de valores a devolver. Esto es necesario debido al orden de las cláusulas values() y annotate().

Si la cláusula values() precede a la cláusula annotate(), se agregarán automáticamente las anotaciones al conjunto de resultados. Sin embargo, si la cláusula values() se aplica después que la cláusula annotate(), debes incluir explícitamente el campo agregado.

Interacción con order_by()

Los campos mencionados en la parte de order_by() de un conjunto de consultas se utilizan al seleccionar los datos de salida, incluso si no se especificaron de otra manera en la llamada a values(). Estos campos adicionales se utilizan para agrupar resultados «similares» entre sí y pueden hacer que las filas de resultado idénticas aparezcan como separadas. Esto se manifiesta, particularmente, al contar cosas.

Por ejemplo, supongamos que tienes un modelo así:

from django.db import models


class Item(models.Model):
    name = models.CharField(max_length=10)
    data = models.IntegerField()

Si deseas contar cuántas veces aparece cada valor distinto data en un conjunto de consultas ordenado, podrías intentar esto:

items = Item.objects.order_by("name")
# Warning: not quite correct!
items.values("data").annotate(Count("id"))

…lo que agrupará los objetos Item por sus valores comunes data y luego contará el número de valores id en cada grupo. Excepto que no funcionará exactamente como se espera. La ordenación por name también jugará un papel en la agrupación, por lo que esta consulta agrupará por pares distintos (data, name), lo cual no es lo que deseas. En su lugar, deberías construir este conjunto de consultas:

items.values("data").annotate(Count("id")).order_by()

Eliminación de cualquier ordenamiento en la consulta. También podrías ordenar por, digamos, data sin efectos perjudiciales, ya que ese ya está jugando un papel en la consulta.

Este comportamiento es el mismo que se indica en la documentación de la consulta distinct() y la regla general es la misma: normalmente no querrás que las columnas adicionales participen en el resultado, por lo que elimina la ordenación o asegúrate al menos de que esté restringida solo a los campos que también se seleccionan en una llamada a values().

Nota

Puedes preguntarte razonablemente por qué Django no elimina las columnas innecesarias por ti. La razón principal es la consistencia con distinct() y otros lugares: Django nunca elimina las restricciones de ordenación que has especificado de manera explícita con order_by() (y no podemos cambiar el comportamiento de los otros métodos, ya que eso violaría nuestra política de La estabilidad de la API.

Ordenación predeterminada no aplicada al GROUP BY

Consultas de agrupación (:guillemets:`GROUP BY`) (por ejemplo, aquellas que utilizan values() y annotate()) no utilizan el ordenamiento predeterminado del modelo. Utiliza explícitamente order_by() cuando se necesita un orden específico.

Anotaciones agrupadas

Puedes generar un agregado sobre el resultado de una annotación. Cuando defines una cláusula aggregate(), los agregados que proporciones pueden referirse a cualquier alias definido como parte de una cláusula annotate() en la consulta.

Ejemplo: si deseas calcular el número promedio de autores por libro, primero anota la colección de libros con el recuento de autor, luego agrupa ese recuento de autor, haciendo referencia al campo de anotación:

>>> from django.db.models import Avg, Count
>>> Book.objects.annotate(num_authors=Count("authors")).aggregate(Avg("num_authors"))
{'num_authors__avg': 1.66}

Agregando sobre consultas vacías o grupos

Cuando se aplica una agregación a un conjunto de resultados vacío o a un agrupado, el resultado recae en su parámetro default por defecto, típicamente None. Este comportamiento ocurre porque las funciones de agregación devuelven NULL cuando la consulta ejecutada no devuelve filas.

Puedes especificar un valor de retorno proporcionando el argumento default para la mayoría de las agregaciones. Sin embargo, ya que Count no admite el argumento default, siempre devolverá 0 para consultas vacías o grupos.

Por ejemplo, suponiendo que ninguna libro contiene web en su nombre, calcular el precio total para este conjunto de libros devolvería None ya que no hay filas coincidentes para computar la agregación Sum sobre:

>>> from django.db.models import Sum
>>> Book.objects.filter(name__contains="web").aggregate(Sum("price"))
{"price__sum": None}

Sin embargo, se puede establecer el argumento default cuando se llama a Sum para devolver un valor de defecto diferente si no se encuentran libros:

>>> Book.objects.filter(name__contains="web").aggregate(Sum("price", default=0))
{"price__sum": Decimal("0")}

Bajo la capa, el argumento default está implementado envolviendo la función agregada con Coalesce.