Django equivalente para contagem e agrupamento por

91

Tenho um modelo parecido com este:

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

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

Quero selecionar a contagem (apenas a contagem) de itens para cada categoria, portanto, no SQL seria tão simples quanto isto:

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

Existe um equivalente a fazer isso "do jeito Django"? Ou o SQL simples é a única opção? Estou familiarizado com o método count () no Django, porém não vejo como group by se encaixaria lá.

Sergey Golovchenko
fonte
Possível duplicata de Como consultar como GROUP BY no Django?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@CiroSantilli 巴拿馬 文件 六四 事件 法轮功 como isso é uma duplicata? esta pergunta foi feita em 2008, e a que você está se referindo é 2 anos depois.
Sergey Golovchenko
O consenso atual é fechar por "qualidade": < meta.stackexchange.com/questions/147643/… > Como "qualidade" não é mensurável, eu apenas considero votos positivos . ;-) Provavelmente se trata de qual pergunta atingiu as melhores palavras-chave newbie do Google no título.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respostas:

131

Aqui, como acabei de descobrir, é como fazer isso com a API de agregação Django 1.1:

from django.db.models import Count
theanswer = Item.objects.values('category').annotate(Count('category'))
Michael
fonte
3
como a maioria das coisas no Django, nada disso faz sentido de se olhar, mas (ao contrário da maioria das coisas no Django) uma vez que eu realmente tentei, foi incrível: P
jsh
3
observe que você precisa usar order_by()se 'category'não for a ordem padrão. (Veja a resposta mais abrangente de Daniel.)
Rick Westera
A razão pela qual isso funciona é porque .annotate()funciona de maneira um pouco diferente após.values() : "No entanto, quando uma cláusula values ​​() é usada para restringir as colunas que são retornadas no conjunto de resultados, o método para avaliar as anotações é um pouco diferente. Em vez de retornar uma anotação resultado para cada resultado no QuerySet original, os resultados originais são agrupados de acordo com as combinações exclusivas dos campos especificados na cláusula values ​​(). "
mgalgs
58

( Atualização : o suporte de agregação ORM completo agora está incluído no Django 1.1 . Fiel ao aviso abaixo sobre o uso de APIs privadas, o método documentado aqui não funciona mais nas versões pós-1.1 do Django. Eu não investiguei o porquê; se estiver no 1.1 ou posterior, você deve usar a API de agregação real de qualquer maneira.)

O suporte de agregação central já estava lá em 1.0; é apenas não documentado, sem suporte e ainda não tem uma API amigável além dele. Mas aqui está como você pode usá-lo mesmo assim até 1.1 chegar (por sua própria conta e risco, e com pleno conhecimento de que o atributo query.group_by não faz parte de uma API pública e pode mudar):

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

Se você iterar em query_set, cada valor retornado será um dicionário com uma chave de "categoria" e uma chave de "contagem".

Você não precisa ordenar por -count aqui, que está incluído apenas para demonstrar como é feito (tem que ser feito na chamada .extra (), não em outro lugar na cadeia de construção do queryset). Além disso, você também poderia dizer contagem (id) em vez de contagem (1), mas o último pode ser mais eficiente.

Note também que ao configurar .query.group_by, os valores devem ser nomes de colunas reais do banco de dados ('category_id') e não nomes de campos do Django ('category'). Isso ocorre porque você está ajustando a parte interna da consulta em um nível em que tudo está em termos de banco de dados, não em termos de Django.

Carl Meyer
fonte
1 para o método antigo. Mesmo que atualmente sem suporte, é esclarecedor para dizer o mínimo. Realmente incrível.
ataque aéreo de
Dê uma olhada na API de agregação do Django em docs.djangoproject.com/en/dev/topics/db/aggregation/… outras tarefas complexas podem ser feitas com ele, lá você encontrará alguns exemplos poderosos.
serfer2 de
@ serfer2 sim, esses documentos já estão vinculados no início desta resposta.
Carl Meyer
56

Como eu estava um pouco confuso sobre como o agrupamento no Django 1.1 funciona, pensei em elaborar aqui como exatamente você o usará. Primeiro, para repetir o que Michael disse:

Aqui, como acabei de descobrir, é como fazer isso com a API de agregação Django 1.1:

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

Observe também que você precisa from django.db.models import Count!

Isso selecionará apenas as categorias e, em seguida, adicionará uma anotação chamada category__count. Dependendo da ordem padrão, isso pode ser tudo de que você precisa, mas se a ordem padrão usar um campo diferente categorydeste não funcionará . A razão para isso é que os campos obrigatórios para o pedido também são selecionados e tornam cada linha única, de modo que você não agrupará os itens como deseja. Uma maneira rápida de corrigir isso é redefinir o pedido:

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

Isso deve produzir exatamente os resultados desejados. Para definir o nome da anotação, você pode usar:

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

Então você terá uma anotação chamada mycountnos resultados.

Tudo o mais sobre agrupamento era muito direto para mim. Certifique-se de verificar a API de agregação do Django para informações mais detalhadas.

Daniel
fonte
1
para executar o mesmo conjunto de ações no campo de chave estrangeira Item.objects.values ​​('category__category'). annotate (Count ('category__category')). order_by ()
Mutant
Como se determina qual é o campo de ordenação padrão?
Bogatyr de
2

Como é isso? (Diferente de lento.)

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

Tem a vantagem de ser curto, mesmo que busque muitas linhas.


Editar.

A versão de uma consulta. BTW, isso geralmente é mais rápido do que SELECT COUNT (*) no banco de dados. Experimente para ver.

counts = defaultdict(int)
for i in Item.objects.all():
    counts[i.category] += 1
S.Lott
fonte
É bom e curto, no entanto, gostaria de evitar ter uma chamada de banco de dados separada para cada categoria.
Sergey Golovchenko
Esta é uma abordagem muito boa para casos simples. Ele cai quando você tem um grande conjunto de dados e deseja ordenar + limitar (ou seja, paginar) de acordo com uma contagem, sem puxar para baixo toneladas de dados desnecessários.
Carl Meyer
@Carl Meyer: Verdade - pode ser canino para um grande conjunto de dados; você precisa fazer um benchmark para ter certeza disso, entretanto. Além disso, também não depende de coisas sem suporte; ele funciona nesse ínterim até que os recursos não suportados sejam suportados.
S.Lott