O Django seleciona apenas linhas com valores de campo duplicados

96

suponha que temos um modelo em Django definido da seguinte forma:

class Literal:
    name = models.CharField(...)
    ...

O campo de nome não é exclusivo e, portanto, pode ter valores duplicados. Preciso realizar a seguinte tarefa: Selecionar todas as linhas do modelo que tenham pelo menos um valor duplicado do namecampo.

Eu sei fazer isso usando SQL simples (pode não ser a melhor solução):

select * from literal where name IN (
    select name from literal group by name having count((name)) > 1
);

Então, é possível selecionar isso usando django ORM? Ou melhor solução SQL?

dragão
fonte

Respostas:

193

Experimentar:

from django.db.models import Count
Literal.objects.values('name')
               .annotate(Count('id')) 
               .order_by()
               .filter(id__count__gt=1)

Isso é o mais próximo que você pode chegar com Django. O problema é que isso retornará um ValuesQuerySetcom apenas namee count. No entanto, você pode usar isso para construir um regular QuerySetalimentando-o de volta em outra consulta:

dupes = Literal.objects.values('name')
                       .annotate(Count('id'))
                       .order_by()
                       .filter(id__count__gt=1)
Literal.objects.filter(name__in=[item['name'] for item in dupes])
Chris Pratt
fonte
5
Provavelmente você quis dizer Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragão
A consulta original dáCannot resolve keyword 'id_count' into field
dragão
2
Obrigado pela resposta atualizada, acho que vou ficar com esta solução, você pode até fazer isso sem a compreensão da lista usandovalues_list('name', flat=True)
dragoon
1
O Django tinha um bug anteriormente (pode ter sido corrigido em versões recentes) onde se você não especificar um nome de campo para a Countanotação a ser salva, o padrão é [field]__count. No entanto, essa sintaxe de sublinhado duplo também é a forma como o Django interpreta que você deseja fazer uma junção. Então, essencialmente quando você tenta filtrar isso, Django pensa que você está tentando fazer uma junção com a countqual obviamente não existe. A correção é especificar um nome para o resultado da sua anotação, ou seja, annotate(mycount=Count('id'))e então filtrar mycount.
Chris Pratt
1
se você adicionar outra chamada para values('name')depois de sua chamada para anotar, você pode remover a compreensão da lista e dizer o Literal.objects.filter(name__in=dupes)que permitirá que tudo seja executado em uma única consulta.
Piper Merriam
43

Isso foi rejeitado como uma edição. Então aqui está como uma resposta melhor

dups = (
    Literal.objects.values('name')
    .annotate(count=Count('id'))
    .values('name')
    .order_by()
    .filter(count__gt=1)
)

Isso retornará um ValuesQuerySetcom todos os nomes duplicados. No entanto, você pode usar isso para construir um regular QuerySetalimentando-o de volta em outra consulta. O django ORM é inteligente o suficiente para combiná-los em uma única consulta:

Literal.objects.filter(name__in=dups)

A chamada extra para .values('name')após a chamada de anotação parece um pouco estranha. Sem isso, a subconsulta falha. Os valores extras enganam o ORM, fazendo-o selecionar apenas a coluna de nome para a subconsulta.

Piper Merriam
fonte
Bom truque, infelizmente só funcionará se apenas um valor for usado (por exemplo, se 'nome' e 'telefone' forem usados, a última parte não funcionará).
guival
1
Para que serve .order_by()?
stefanfoulis
4
@stefanfoulis Limpa qualquer pedido existente. Se você tiver uma ordem definida por modelo, isso se tornará parte da GROUP BYcláusula SQL e quebrará as coisas. Descobri isso ao jogar com Subquery (na qual você faz agrupamentos muito semelhantes via .values())
Oli
10

tente usar agregação

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
JamesO
fonte
Ok, isso dá a lista de nomes correta, mas é possível selecionar ids e outros campos ao mesmo tempo?
dragão
@dragoon - não, mas Chris Pratt abordou a alternativa em sua resposta.
JamesO
5

Caso você use PostgreSQL, você pode fazer algo assim:

from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Func, Value

duplicate_ids = (Literal.objects.values('name')
                 .annotate(ids=ArrayAgg('id'))
                 .annotate(c=Func('ids', Value(1), function='array_length'))
                 .filter(c__gt=1)
                 .annotate(ids=Func('ids', function='unnest'))
                 .values_list('ids', flat=True))

Isso resulta nesta consulta SQL bastante simples:

SELECT unnest(ARRAY_AGG("app_literal"."id")) AS "ids"
FROM "app_literal"
GROUP BY "app_literal"."name"
HAVING array_length(ARRAY_AGG("app_literal"."id"), 1) > 1
Eugene Pakhomov
fonte
0

Se você deseja resultar apenas na lista de nomes, mas não nos objetos, você pode usar a seguinte consulta

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
user2959723
fonte