Como consultar como GROUP BY no django?

333

Eu consulto um modelo:

Members.objects.all()

E retorna:

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

O que eu quero é conhecer a melhor maneira do Django de disparar uma group_byconsulta ao meu banco de dados, como:

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

O que não funciona, é claro. Eu sei que podemos fazer alguns truques django/db/models/query.py, mas estou curioso para saber como fazê-lo sem aplicar patches.

simplyharsh
fonte

Respostas:

484

Se você deseja fazer agregação, pode usar os recursos de agregação do ORM :

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

Isso resulta em uma consulta semelhante a

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

e a saída seria da forma

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]
Guðmundur H
fonte
6
@ Harry: Você pode encadeá-lo. Algo como:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Eli
57
Eu tenho uma pergunta, esta consulta está retornando apenas a designação e o dcount, e se eu quiser obter outros valores da tabela também?
AJ
19
Observe que, se sua classificação for um campo diferente da designação, ela não funcionará sem redefinir a classificação. Veja stackoverflow.com/a/1341667/202137
Gidgidonihah 5/05
12
@Gidgidonihah É verdade, o exemplo deve lerMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix
7
Eu tenho uma pergunta, esta consulta está retornando apenas designação e dcount, e se eu quiser obter outros valores da tabela também?
precisa
55

Uma solução fácil, mas não a maneira correta, é usar o SQL bruto :

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

Outra solução é usar a group_bypropriedade:

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

Agora você pode iterar sobre a variável results para recuperar seus resultados. Note que group_bynão está documentado e pode ser alterado na versão futura do Django.

E ... por que você quer usar group_by? Se você não usar agregação, poderá usar order_bypara obter um resultado semelhante.

Michael
fonte
Você pode me dizer como fazê-lo usando order_by ??
22420
2
Olá, se você não estiver usando agregação, poderá emular group_by usando um order_by e eliminar as entradas desnecessárias. Obviamente, essa é uma emulação e só pode ser usada quando não há muitos dados. Como ele não falou em agregação, pensei que poderia ser uma solução.
Michael
Hey isso é ótimo - você pode explicar como o execute_sql uso ele não aparece para trabalhar ..
rh0dium
8
Note que isso não funciona mais no Django 1.9. stackoverflow.com/questions/35558120/…
grokpot 8/17
1
Essa é uma maneira de hack-ish de usar o ORM. Não é necessário instanciar novos conjuntos de consultas que passam manualmente pelas antigas.
21918 Ian Kirkpatrick
32

Você também pode usar a regrouptag de modelo para agrupar por atributos. Dos 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 parece com isso:

  • Índia
    • Mumbai: 19.000.000
    • Calcutá: 15.000.000
  • EUA
    • Nova York: 20.000.000
    • Chicago: 7.000.000
  • Japão
    • Tóquio: 33.000.000

Também funciona em QuerySets eu acredito.

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

editar: observe que a regrouptag não funciona como seria de esperar se sua lista de dicionários não for classificada por chave. Funciona iterativamente. Portanto, classifique sua lista (ou conjunto de consultas) pela chave da garoupa antes de passá-la para a regrouptag.

inostia
fonte
1
Isto é perfeito! Eu procurei muito por uma maneira simples de fazer isso. E funciona também em conjuntos de consultas, foi assim que eu o usei.
Carmena
1
isso é totalmente errado se você ler um grande conjunto de dados do banco de dados e depois usar valores agregados.
Sławomir Lenart
Certamente, isso pode não ser tão eficiente quanto uma consulta direta ao banco de dados. Mas para casos de uso simples que pode ser uma boa solução
inostia
Isso funcionará se o resultado mostrado no modelo. Mas, para JsonResponse ou outra resposta indireta. esta solução não irá funcionar.
Willy satrio nugroho
1
@Willysatrionugroho se você quiser fazê-lo em uma visão, por exemplo, stackoverflow.com/questions/477820/... pode funcionar para você
inostia
7

Você precisa fazer o SQL customizado, conforme exemplificado neste trecho:

SQL personalizado via subconsulta

Ou em um gerenciador personalizado, como mostrado nos documentos on-line do Django:

Adicionando métodos extras do Manager

Van Gale
fonte
1
Tipo de solução de ida e volta. Eu teria usado, se eu tivesse algum uso prolongado disso. Mas aqui eu só preciso do número de membros por designação, isso é tudo.
23420
Sem problemas. Eu pensei em mencionar 1.1 recursos de agregação, mas fez a suposição de que você estava usando a versão :)
Van Gale
É tudo sobre o uso de consultas brutas, que mostram a fraqueza do ORM do Django.
Sławomir Lenart
5

O Django não suporta grupo livre por consultas . Eu aprendi isso da pior maneira possível. O ORM não foi projetado para oferecer suporte a coisas como o que você deseja fazer, sem usar o SQL personalizado. Você está limitado a:

  • Sql RAW (ou seja, MyModel.objects.raw ())
  • cr.execute sentenças (e uma análise feita à mão do resultado).
  • .annotate() (o grupo por sentenças é realizado no modelo filho para .annotate (), em exemplos como agregando lines_count = Count ('lines'))).

Em um conjunto de consultas, qsvocê pode chamar, qs.query.group_by = ['field1', 'field2', ...]mas é arriscado se você não souber qual consulta está editando e não tem garantia de que funcionará e não interromperá as internas do objeto QuerySet. Além disso, é uma API interna (não documentada) que você não deve acessar diretamente sem arriscar que o código não seja mais compatível com futuras versões do Django.

Luis Masuelli
fonte
na verdade, você está limitado não apenas ao agrupar gratuitamente, então tente o SQLAlchemy em vez do Django ORM.
Sławomir Lenart
5

Há um módulo que permite agrupar modelos do Django e ainda trabalhar com um QuerySet no resultado: https://github.com/kako-nawao/django-group-by

Por exemplo:

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>

A diferença para as consultas annotate/ aggregateDjango básicas é o uso dos atributos de um campo relacionado, por exemplo book.author.last_name.

Se você precisar das PKs das instâncias que foram agrupadas, adicione a seguinte anotação:

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

NOTA: ArrayAggé uma função específica do Postgres, disponível a partir do Django 1.9: https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/aggregates/#arrayagg

Risadinha
fonte
Esse django-group-by é uma alternativa ao valuesmétodo. É para um propósito diferente, eu acho.
LShi
1
@LShi Não é uma alternativa aos valores, claro que não. valuesé um SQL selectenquanto group_byé um SQL group by(como o nome indica ...). Por que o voto negativo? Estamos usando esse código em produção para implementar group_byinstruções complexas .
Risadinha
O documento diz que group_by"se comporta principalmente como o método values, mas com uma diferença ..." O documento não menciona SQL GROUP BYe o caso de uso que ele fornece não sugere que isso tenha algo a ver com SQL GROUP BY. Retiro a votação quando alguém deixar isso claro, mas esse documento é realmente enganoso.
LShi
Depois de ler o documentovalues , descobri que perdi que valuesfunciona como um GROUP BY. É minha culpa. Eu acho que é mais simples de usar do itertools.groupbyque esse django-group-by quando valuesé insuficiente.
LShi
1
É impossível fazer isso group byde cima com uma simples valueschamada - com ou sem annotatee sem buscar tudo no banco de dados. Sua sugestão de itertools.groupbytrabalhos para conjuntos de dados pequenos, mas não para vários milhares de conjuntos de dados que você provavelmente deseja paginar. Obviamente, nesse ponto, você terá que pensar em um índice de pesquisa especial que contenha dados preparados (já agrupados), de qualquer maneira.
Risadinha
0

O documento diz que você pode usar valores para agrupar o 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

Você pode encontrar todos os livros e agrupá-los por nome usando este código:

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

Você pode assistir a algumas folhas aqui .

Ramwin
fonte
-1

Se não me engano, você pode usar o que quer que seja o conjunto de consultas .group_by = [' field ']

Reed Jones
fonte
8
Este não é o caso, pelo menos no Django 1.6: objeto 'QuerySet' tem nenhum atributo 'group_by'
Facundo Olano
1
Um uso adequado pode ser queryset.query.group_by = [...] mas isso interromperia a semântica da consulta e não funcionaria conforme o esperado.
Luis Masuelli
-2
from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

primeiro você precisa importar Sum então ..

Kiran S canal do youtube
fonte