Django seleziona solo le righe con valori di campo duplicati


96

supponiamo di avere un modello in django definito come segue:

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

Il campo Nome non è univoco e quindi può avere valori duplicati. È necessario eseguire la seguente attività: selezionare tutte le righe dal modello che hanno almeno un valore duplicato del namecampo.

So come farlo usando un semplice SQL (potrebbe non essere la soluzione migliore):

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

Quindi, è possibile selezionarlo utilizzando django ORM? O una migliore soluzione SQL?

Risposte:


193

Provare:

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

Questo è il più vicino possibile a Django. Il problema è che questo restituirà un ValuesQuerySetcon solo namee count. Tuttavia, puoi quindi usarlo per costruire un normale QuerySetreinserendolo in un'altra query:

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

5
Probabilmente intendevi Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragone

La query originale dàCannot resolve keyword 'id_count' into field
dragoon

2
Grazie per la risposta aggiornata, penso che continuerò con questa soluzione, puoi anche farlo senza la comprensione della lista usandovalues_list('name', flat=True)
dragoon

1
Django aveva in precedenza un bug su questo (potrebbe essere stato corretto nelle versioni recenti) dove se non specifichi un nome di campo per l' Countannotazione da salvare come, il valore predefinito è [field]__count. Tuttavia, quella sintassi con doppio trattino basso è anche il modo in cui Django interpreta il fatto che tu voglia fare un join. Quindi, essenzialmente quando provi a filtrare su questo, Django pensa che stai cercando di fare un join con il countquale ovviamente non esiste. La soluzione consiste nello specificare un nome per il risultato dell'annotazione, cioè annotate(mycount=Count('id'))e quindi filtrare mycountinvece.
Chris Pratt

1
se aggiungi un'altra chiamata a values('name')dopo la tua chiamata per annotare, puoi rimuovere la comprensione dell'elenco e dire Literal.objects.filter(name__in=dupes)che consentirà a tutto ciò di essere eseguito in una singola query.
Piper Merriam

43

Questa è stata rifiutata come modifica. Quindi eccola qui come una risposta migliore

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

Ciò restituirà un ValuesQuerySetcon tutti i nomi duplicati. Tuttavia, puoi quindi usarlo per costruire un normale QuerySetreinserendolo in un'altra query. Django ORM è abbastanza intelligente da combinarli in una singola query:

Literal.objects.filter(name__in=dups)

La chiamata extra a .values('name')dopo la chiamata di annotazione sembra un po 'strana. Senza questo, la sottoquery fallisce. I valori aggiuntivi inducono l'ORM a selezionare solo la colonna del nome per la sottoquery.


Bel trucco, sfortunatamente funzionerà solo se viene utilizzato un solo valore (ad es. Se vengono utilizzati sia "nome" che "telefono", l'ultima parte non funzionerebbe).
Guival

1
A cosa serve .order_by()?
stefanfoulis

4
@stefanfoulis Cancella qualsiasi ordine esistente. Se si dispone di un ordinamento del set di modelli, questo diventa parte della GROUP BYclausola SQL e ciò rompe le cose. L'ho scoperto giocando con Subquery (in cui fai raggruppamenti molto simili tramite .values())
Oli

10

prova a utilizzare l' aggregazione

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)

Ok, questo fornisce l'elenco dei nomi corrent, ma è possibile selezionare ID e altri campi contemporaneamente?
dragone

@dragoon - no, ma Chris Pratt ha coperto l'alternativa nella sua risposta.
JamesO

5

Nel caso in cui utilizzi PostgreSQL, puoi fare qualcosa del genere:

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

Il risultato è questa query SQL piuttosto semplice:

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

0

Se si desidera ottenere solo l'elenco dei nomi ma non gli oggetti, è possibile utilizzare la seguente query

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.