¿Cómo consultar como GROUP BY en django?

333

Consulto un modelo:

Members.objects.all()

Y vuelve:

Eric, Salesman, X-Shop
Freddie, Manager, X2-Shop
Teddy, Salesman, X2-Shop
Sean, Manager, X2-Shop

Lo que quiero es conocer la mejor forma de Django para enviar una group_byconsulta a mi base de datos, como:

Members.objects.all().group_by('designation')

Lo cual no funciona, por supuesto. Sé que podemos hacer algunos trucos django/db/models/query.py, pero tengo curiosidad por saber cómo hacerlo sin parches.

simplemente duro
fuente

Respuestas:

484

Si quiere hacer una agregación, puede usar las funciones de agregación del ORM :

from django.db.models import Count
Members.objects.values('designation').annotate(dcount=Count('designation'))

Esto da como resultado una consulta similar a

SELECT designation, COUNT(designation) AS dcount
FROM members GROUP BY designation

y la salida sería de la forma

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]
Guðmundur H
fuente
66
@Harry: Puedes encadenarlo. Algo así como:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Eli
57
Tengo una pregunta, esta consulta solo devuelve designación y dcount, ¿qué sucede si también quiero obtener otros valores de la tabla?
AJ
19
Tenga en cuenta que si su clasificación es un campo distinto de la designación, no funcionará sin restablecer la clasificación. Ver stackoverflow.com/a/1341667/202137
Gidgidonihah
12
@Gidgidonihah Cierto, el ejemplo debería leerMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix
77
Tengo una pregunta, esta consulta solo devuelve designación y dcount, ¿qué sucede si también quiero obtener otros valores de la tabla?
Yann 叶
55

Una solución fácil, pero no la forma correcta es usar SQL sin formato :

results = Members.objects.raw('SELECT * FROM myapp_members GROUP BY designation')

Otra solución es usar la group_bypropiedad:

query = Members.objects.all().query
query.group_by = ['designation']
results = QuerySet(query=query, model=Members)

Ahora puede iterar sobre la variable de resultados para recuperar sus resultados. Tenga en cuenta que group_byno está documentado y puede modificarse en futuras versiones de Django.

Y ... ¿por qué quieres usar group_by? Si no usa la agregación, puede usarla order_bypara lograr un resultado similar.

Michael
fuente
¿Puedes decirme cómo hacerlo usando order_by?
simplemente duro
2
Hola, si no está utilizando la agregación, puede emular group_by utilizando un order_by y eliminar las entradas que no necesita. Por supuesto, esta es una emulación y solo se puede usar cuando no se usan muchos datos. Como no habló de agregación, pensé que podría ser una solución.
Michael
Hey esto es genial - Puede usted explicar cómo el uso execute_sql que no parece que el trabajo ..
rh0dium
8
Tenga en cuenta que esto ya no funciona en Django 1.9. stackoverflow.com/questions/35558120/…
grokpot
1
Esta es una forma hack-ish de usar el ORM. No debería tener que crear instancias de nuevos conjuntos de consultas que pasen los antiguos manualmente.
Ian Kirkpatrick
33

También puede usar la regroupetiqueta de plantilla para agrupar por atributos. De los documentos:

cities = [
    {'name': 'Mumbai', 'population': '19,000,000', 'country': 'India'},
    {'name': 'Calcutta', 'population': '15,000,000', 'country': 'India'},
    {'name': 'New York', 'population': '20,000,000', 'country': 'USA'},
    {'name': 'Chicago', 'population': '7,000,000', 'country': 'USA'},
    {'name': 'Tokyo', 'population': '33,000,000', 'country': 'Japan'},
]

...

{% regroup cities by country as country_list %}

<ul>
    {% for country in country_list %}
        <li>{{ country.grouper }}
            <ul>
            {% for city in country.list %}
                <li>{{ city.name }}: {{ city.population }}</li>
            {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

Se ve como esto:

  • India
    • Mumbai: 19,000,000
    • Calcuta: 15,000,000
  • Estados Unidos
    • Nueva York: 20,000,000
    • Chicago: 7,000,000
  • Japón
    • Tokio: 33,000,000

También funciona en QuerySets, creo.

fuente: https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#regroup

editar: tenga en cuenta que la regroupetiqueta no funciona como cabría esperar si su lista de diccionarios no está ordenada por clave. Funciona iterativamente. Así que ordene su lista (o conjunto de consultas) por la clave del mero antes de pasarla a la regroupetiqueta.

inostia
fuente
1
¡Esto es perfecto! He buscado mucho por una manera simple de hacer esto. Y también funciona en conjuntos de consultas, así es como lo usé.
CarmenA
1
esto es totalmente incorrecto si lee de la base de datos un gran conjunto de datos y luego solo usa valores agregados.
Sławomir Lenart
@ SławomirLenart seguro, esto podría no ser tan eficiente como una consulta de base de datos directa. Pero para casos de uso simples puede ser una buena solución
inostia el
Esto funcionará si el resultado se muestra en la plantilla. Pero, para JsonResponse u otra respuesta indirecta. Esta solución no funcionará.
Willy satrio nugroho
1
@Willysatrionugroho si quisiera hacerlo en una vista, por ejemplo, stackoverflow.com/questions/477820/… podría funcionar para usted
inostia
7

Necesita hacer SQL personalizado como se ejemplifica en este fragmento:

SQL personalizado a través de subconsulta

O en un administrador personalizado como se muestra en los documentos en línea de Django:

Agregar métodos de administrador adicionales

Van Gale
fuente
1
Tipo de solución de ida y vuelta. Lo hubiera usado, si hubiera tenido un uso prolongado de eso. Pero aquí solo necesito el número de miembros por designación, eso es todo.
simplemente duro
No hay problema. Pensé en mencionar 1.1 Características de agregación, pero hizo la suposición de que estaba utilizando la versión de lanzamiento :)
Van Gale
Se trata de utilizar consultas sin procesar, que muestran la debilidad del ORM de Django.
Sławomir Lenart
5

Django no admite grupos libres por consultas . Lo aprendí de la muy mala manera. ORM no está diseñado para admitir cosas como lo que desea hacer, sin usar SQL personalizado. Estás limitado a:

  • RAW sql (es decir, MyModel.objects.raw ())
  • cr.execute oraciones (y un análisis del resultado hecho a mano).
  • .annotate() (el grupo por oraciones se realiza en el modelo secundario para .annotate (), en ejemplos como agregar lines_count = Count ('lines'))).

qsPuede llamar a un conjunto de consultas, qs.query.group_by = ['field1', 'field2', ...]pero es arriesgado si no sabe qué consulta está editando y no tiene garantía de que funcionará y no romperá las partes internas del objeto QuerySet. Además, es una API interna (no documentada) a la que no debe acceder directamente sin arriesgarse a que el código ya no sea compatible con futuras versiones de Django.

Luis Masuelli
fuente
de hecho, está limitado no solo en el grupo libre, así que pruebe SQLAlchemy en lugar de Django ORM.
Sławomir Lenart
5

Hay un módulo que le permite agrupar modelos de Django y seguir trabajando con un QuerySet en el resultado: https://github.com/kako-nawao/django-group-by

Por ejemplo:

from django_group_by import GroupByMixin

class BookQuerySet(QuerySet, GroupByMixin):
    pass

class Book(Model):
    title = TextField(...)
    author = ForeignKey(User, ...)
    shop = ForeignKey(Shop, ...)
    price = DecimalField(...)

class GroupedBookListView(PaginationMixin, ListView):
    template_name = 'book/books.html'
    model = Book
    paginate_by = 100

    def get_queryset(self):
        return Book.objects.group_by('title', 'author').annotate(
            shop_count=Count('shop'), price_avg=Avg('price')).order_by(
            'name', 'author').distinct()

    def get_context_data(self, **kwargs):
        return super().get_context_data(total_count=self.get_queryset().count(), **kwargs)

'book / books.html'

<ul>
{% for book in object_list %}
    <li>
        <h2>{{ book.title }}</td>
        <p>{{ book.author.last_name }}, {{ book.author.first_name }}</p>
        <p>{{ book.shop_count }}</p>
        <p>{{ book.price_avg }}</p>
    </li>
{% endfor %}
</ul>

La diferencia con las consultas annotate/ aggregatebasic Django es el uso de los atributos de un campo relacionado, por ejemplo book.author.last_name.

Si necesita las PK de las instancias que se han agrupado, agregue la siguiente anotación:

.annotate(pks=ArrayAgg('id'))

NOTA: ArrayAgges una función específica de Postgres, disponible desde Django 1.9 en adelante: https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/aggregates/#arrayagg

Risadinha
fuente
Este django-group-by es una alternativa al valuesmétodo. Es para un propósito diferente, creo.
LShi
1
@LShi No es una alternativa a los valores, por supuesto que no. valueses un SQL selectmientras que group_byes un SQL group by(como su nombre indica ...). ¿Por qué el voto negativo? Estamos utilizando dicho código en producción para implementar group_bydeclaraciones complejas .
Risadinha
Su documento dice group_by"se comporta principalmente como el método de valores, pero con una diferencia ..." El documento no menciona SQL GROUP BYy el caso de uso que proporciona no sugiere que tenga nada que ver con SQL GROUP BY. Retiraré el voto negativo cuando alguien lo haya dejado claro, pero ese documento es realmente engañoso.
LShi
Después de leer el documento paravalues , descubrí que extrañaba que valuesfunciona como un GROUP BY. Que es mi culpa. Creo que es más fácil de usar itertools.groupbyque este django-group-by cuando valueses insuficiente.
LShi
1
Es imposible hacer lo group byanterior con una simple valuesllamada -con o sin annotatey sin obtener todo de la base de datos. Su sugerencia de itertools.groupbytrabajos para pequeños conjuntos de datos, pero no para varios miles de conjuntos de datos que probablemente desee buscar. Por supuesto, en ese momento tendrá que pensar en un índice de búsqueda especial que contenga datos preparados (ya agrupados) de todos modos.
Risadinha
0

El documento dice que puede usar valores para agrupar el conjunto de consultas.

class Travel(models.Model):
    interest = models.ForeignKey(Interest)
    user = models.ForeignKey(User)
    time = models.DateTimeField(auto_now_add=True)

# Find the travel and group by the interest:

>>> Travel.objects.values('interest').annotate(Count('user'))
<QuerySet [{'interest': 5, 'user__count': 2}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited for 2 times, 
# and the interest(id=6) had only been visited for 1 time.

>>> Travel.objects.values('interest').annotate(Count('user', distinct=True)) 
<QuerySet [{'interest': 5, 'user__count': 1}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited by only one person (but this person had 
#  visited the interest for 2 times

Puede encontrar todos los libros y agruparlos por nombre usando este código:

Book.objects.values('name').annotate(Count('id')).order_by() # ensure you add the order_by()

Puedes ver alguna hoja de cheet aquí .

ramwin
fuente
-1

Si no me estoy equivocando, puedes usar, whatever-query-set .group_by = [' field ']

Reed Jones
fuente
8
Este no es el caso, al menos en Django 1.6: el objeto 'QuerySet' no tiene el atributo 'group_by'
Facundo Olano
1
Un uso adecuado podría ser queryset.query.group_by = [...] pero esto rompería la semántica de la consulta y no funcionaría como se esperaba.
Luis Masuelli
-2
from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

primero necesitas importar Sum y luego ...

Kiran S canal de youtube
fuente