Django selecciona solo filas con valores de campo duplicados

96

supongamos que tenemos un modelo en django definido de la siguiente manera:

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

El campo de nombre no es único y, por lo tanto, puede tener valores duplicados. Necesito realizar la siguiente tarea: Seleccionar todas las filas del modelo que tengan al menos un valor duplicado del namecampo.

Sé cómo hacerlo usando SQL simple (puede que no sea la mejor solución):

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

Entonces, ¿es posible seleccionar esto usando django ORM? ¿O una mejor solución SQL?

dragón
fuente

Respuestas:

192

Tratar:

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

Esto es lo más parecido que puede conseguir con Django. El problema es que esto devolverá ValuesQuerySetsolo con namey count. Sin embargo, puede usar esto para construir un regular QuerySetal retroalimentarlo en otra 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
fuente
4
Probablemente has querido decir Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragón
La consulta original daCannot resolve keyword 'id_count' into field
dragón
2
Gracias por la respuesta actualizada, creo que me quedaré con esta solución, incluso puede hacerlo sin la comprensión de la lista usandovalues_list('name', flat=True)
dragoon
1
Django anteriormente tenía un error en esto (podría haber sido corregido en versiones recientes) donde si no especifica un nombre de campo para que la Countanotación se guarde como, el valor predeterminado es [field]__count. Sin embargo, esa sintaxis de doble subrayado es también la forma en que Django interpreta que desea hacer una combinación. Entonces, esencialmente cuando intentas filtrar eso, Django piensa que estás tratando de hacer una unión con la countque obviamente no existe. La solución es especificar un nombre para el resultado de su anotación, es decir, annotate(mycount=Count('id'))y luego filtrar en su mycountlugar.
Chris Pratt
1
si agrega otra llamada a values('name')después de su llamada para anotar, puede eliminar la comprensión de la lista y decir Literal.objects.filter(name__in=dupes)cuál permitirá que todo esto se ejecute en una sola consulta.
Piper Merriam
42

Esto fue rechazado como edición. Así que aquí está una mejor respuesta.

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

Esto devolverá un ValuesQuerySetcon todos los nombres duplicados. Sin embargo, puede usar esto para construir un regular QuerySetrealimentándolo en otra consulta. El ORM de django es lo suficientemente inteligente como para combinarlos en una sola consulta:

Literal.objects.filter(name__in=dups)

La llamada adicional a .values('name')después de la llamada de anotación parece un poco extraña. Sin esto, la subconsulta falla. Los valores adicionales engañan al ORM para que solo seleccione la columna de nombre para la subconsulta.

Piper Merriam
fuente
Buen truco, desafortunadamente esto solo funcionará si solo se usa un valor (por ejemplo, si se usan tanto 'nombre' como 'teléfono', la última parte no funcionaría).
Guival
1
¿Para qué sirve .order_by()?
stefanfoulis
4
@stefanfoulis Elimina cualquier pedido existente. Si tiene un pedido de conjunto de modelos, esto se convierte en parte de la GROUP BYcláusula SQL y eso rompe las cosas. Lo descubrí al jugar con Subconsulta (en la que haces agrupaciones muy similares a través de .values())
Oli
10

intenta usar agregación

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
JamesO
fuente
Ok, eso da la lista actual de nombres, pero ¿es posible seleccionar identificadores y otros campos al mismo tiempo?
dragón
@dragoon: no, pero Chris Pratt ha cubierto la alternativa en su respuesta.
JamesO
5

En caso de que use PostgreSQL, puede hacer algo como esto:

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))

El resultado es esta consulta SQL bastante simple:

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
fuente
0

Si desea obtener solo la lista de nombres pero no los objetos, puede usar la siguiente consulta

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