Equivalente de Django para contar y agrupar por

91

Tengo un modelo que se parece a esto:

class Category(models.Model):
    name = models.CharField(max_length=60)

class Item(models.Model):
    name = models.CharField(max_length=60)
    category = models.ForeignKey(Category)

Quiero seleccionar el recuento (solo el recuento) de elementos para cada categoría, por lo que en SQL sería tan simple como esto:

select category_id, count(id) from item group by category_id

¿Existe un equivalente a hacer esto "al estilo Django"? ¿O es SQL simple la única opción? Estoy familiarizado con el método count () en Django, sin embargo, no veo cómo encajaría group by allí.

Sergey Golovchenko
fuente
¿Posible duplicado de cómo consultar como GROUP BY en django?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@CiroSantilli 巴拿馬 文件 六四 事件 法轮功 ¿cómo es esto un duplicado? esta pregunta se hizo en 2008, y a la que se refiere es 2 años después.
Sergey Golovchenko
El consenso actual es cerrar por "calidad": < meta.stackexchange.com/questions/147643/… > Dado que la "calidad" no se puede medir, me limito a votar a favor. ;-) Es probable que se reduzca a qué pregunta golpeó a las mejores palabras clave de Google para principiantes en el título.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

131

Aquí, como acabo de descubrir, es cómo hacer esto con la API de agregación Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))
Miguel
fuente
3
como la mayoría de las cosas en Django, nada de esto tiene mucho sentido de ver, pero (a diferencia de la mayoría de las cosas en Django) una vez que lo probé, fue increíble: P
jsh
3
tenga en cuenta que debe usar order_by()si 'category'no es el pedido predeterminado. (Vea la respuesta más completa de Daniel.)
Rick Westera
La razón por la que esto funciona es porque .annotate()funciona de forma ligeramente diferente después de.values() : "Sin embargo, cuando se usa una cláusula de valores () para restringir las columnas que se devuelven en el conjunto de resultados, el método para evaluar las anotaciones es ligeramente diferente. En lugar de devolver una anotación resultado para cada resultado en el QuerySet original, los resultados originales se agrupan de acuerdo con las combinaciones únicas de los campos especificados en la cláusula values ​​(). "
mgalgs
58

( Actualización : el soporte completo de agregación de ORM ahora está incluido en Django 1.1 . Fiel a la siguiente advertencia sobre el uso de API privadas, el método documentado aquí ya no funciona en versiones posteriores a la 1.1 de Django. No he investigado para averiguar por qué; si está en 1.1 o posterior, debe usar la API de agregación real de todos modos).

El soporte de agregación central ya estaba allí en 1.0; simplemente no está documentado, no es compatible y aún no tiene una API amigable. Pero así es como puede usarlo de todos modos hasta que llegue 1.1 (bajo su propio riesgo y con pleno conocimiento de que el atributo query.group_by no es parte de una API pública y podría cambiar):

query_set = Item.objects.extra(select={'count': 'count(1)'}, 
                               order_by=['-count']).values('count', 'category')
query_set.query.group_by = ['category_id']

Si luego itera sobre query_set, cada valor devuelto será un diccionario con una clave de "categoría" y una clave de "recuento".

No tiene que ordenar por recuento aquí, eso solo se incluye para demostrar cómo se hace (debe hacerse en la llamada .extra (), no en otra parte de la cadena de construcción del conjunto de consultas). Además, también podría decir count (id) en lugar de count (1), pero este último puede ser más eficiente.

Tenga en cuenta también que al configurar .query.group_by, los valores deben ser nombres de columna de base de datos reales ('category_id') y no nombres de campo de Django ('categoría'). Esto se debe a que está modificando los aspectos internos de la consulta a un nivel en el que todo está en términos de DB, no en términos de Django.

Carl Meyer
fuente
+1 para el método antiguo. Incluso si actualmente no tiene soporte, es esclarecedor por decir lo menos. Realmente asombroso.
ataque aéreo
Eche un vistazo a la API de agregación de Django en docs.djangoproject.com/en/dev/topics/db/aggregation/… Se pueden realizar otras tareas complejas con él, allí encontrará algunos ejemplos poderosos.
serfer2
@ serfer2 sí, esos documentos ya están vinculados desde la parte superior de esta respuesta.
Carl Meyer
56

Dado que estaba un poco confundido acerca de cómo funciona la agrupación en Django 1.1, pensé en detallar aquí cómo exactamente se usa. Primero, para repetir lo que dijo Michael:

Aquí, como acabo de descubrir, es cómo hacer esto con la API de agregación Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))

¡Tenga en cuenta también que es necesario from django.db.models import Count!

Esto seleccionará solo las categorías y luego agregará una anotación llamada category__count. Dependiendo del orden predeterminado, esto puede ser todo lo que necesita, pero si el orden predeterminado utiliza un campo diferente a categoryeste, no funcionará . La razón de esto es que los campos requeridos para ordenar también están seleccionados y hacen que cada fila sea única, por lo que no obtendrá las cosas agrupadas como lo desea. Una forma rápida de solucionar este problema es restablecer el pedido:

Item.objects.values('category').annotate(Count('category')).order_by()

Esto debería producir exactamente los resultados que desea. Para establecer el nombre de la anotación, puede usar:

...annotate(mycount = Count('category'))...

Entonces tendrás una anotación llamada mycounten los resultados.

Todo lo demás sobre la agrupación fue muy sencillo para mí. Asegúrese de consultar la API de agregación de Django para obtener información más detallada.

Daniel
fuente
1
para realizar el mismo conjunto de acciones en el campo de clave externa Item.objects.values ​​('category__category'). annotate (Count ('category__category')). order_by ()
Mutant
¿Cómo se determina cuál es el campo de pedido predeterminado?
Bogatyr
2

¿Cómo es esto? (Aparte de lento).

counts= [ (c, Item.filter( category=c.id ).count()) for c in Category.objects.all() ]

Tiene la ventaja de ser corto, incluso si obtiene muchas filas.


Editar.

La versión de una consulta. Por cierto, esto suele ser más rápido que SELECT COUNT (*) en la base de datos. Pruébelo para ver.

counts = defaultdict(int)
for i in Item.objects.all():
    counts[i.category] += 1
S.Lott
fuente
Es agradable y breve, sin embargo, me gustaría evitar tener una llamada de base de datos separada para cada categoría.
Sergey Golovchenko
Este es un enfoque realmente bueno para casos simples. Se cae cuando tiene un gran conjunto de datos y desea ordenar + limitar (es decir, paginar) de acuerdo con un recuento, sin extraer toneladas de datos innecesarios.
Carl Meyer
@Carl Meyer: Verdadero, puede ser perrito para un conjunto de datos grande; sin embargo, debe realizar una evaluación comparativa para estar seguro de ello. Además, tampoco depende de material no compatible; funciona mientras tanto hasta que se admitan las funciones no compatibles.
S.Lott