Django wählt nur Zeilen mit doppelten Feldwerten aus

96

Angenommen, wir haben ein Modell in Django, das wie folgt definiert ist:

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

Das Namensfeld ist nicht eindeutig und kann daher doppelte Werte haben. Ich muss die folgende Aufgabe ausführen: Wählen Sie alle Zeilen aus dem Modell aus, die mindestens einen doppelten Wert des nameFelds haben.

Ich weiß, wie man es mit einfachem SQL macht (möglicherweise nicht die beste Lösung):

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

Ist es also möglich, dies mit Django ORM auszuwählen? Oder bessere SQL-Lösung?

Dragoner
quelle

Antworten:

191

Versuchen:

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

Dies ist so nah wie möglich an Django. Das Problem ist, dass dies ValuesQuerySetnur mit nameund zurückgibt count. Sie können dies dann jedoch verwenden, um eine reguläre QuerySetAbfrage zu erstellen, indem Sie sie in eine andere Abfrage zurückführen:

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
quelle
4
Wahrscheinlich hast du gemeint Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
Dragoner
Ursprüngliche Abfrage gibtCannot resolve keyword 'id_count' into field
Dragoner
2
Vielen Dank für die aktualisierte Antwort, ich denke, ich werde bei dieser Lösung bleiben, Sie können es sogar ohne Listenverständnis tun, indem Sievalues_list('name', flat=True)
Dragoner
1
Django hatte zuvor einen Fehler (der möglicherweise in neueren Versionen behoben wurde). Wenn Sie keinen Feldnamen für die CountAnmerkung angeben, unter der gespeichert werden soll, wird standardmäßig der Fehler verwendet [field]__count. Mit dieser Syntax mit doppeltem Unterstrich interpretiert Django jedoch auch, dass Sie einen Join durchführen möchten. Wenn Sie also versuchen, danach zu filtern, denkt Django, dass Sie versuchen, eine Verknüpfung herzustellen, mit countder es offensichtlich keine gibt. Die Korrektur besteht darin, einen Namen für Ihr Anmerkungsergebnis anzugeben, dh stattdessen zu annotate(mycount=Count('id'))filtern mycount.
Chris Pratt
1
Wenn Sie values('name')nach Ihrem Aufruf zum Kommentieren einen weiteren Aufruf hinzufügen , können Sie das Listenverständnis entfernen und angeben, Literal.objects.filter(name__in=dupes)wodurch dies alles in einer einzigen Abfrage ausgeführt werden kann.
Piper Merriam
42

Dies wurde als Bearbeitung abgelehnt. Also hier ist es als bessere Antwort

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

Dies gibt ein ValuesQuerySetmit allen doppelten Namen zurück. Sie können dies dann jedoch verwenden, um eine reguläre QuerySetAbfrage zu erstellen, indem Sie sie in eine andere Abfrage zurückführen. Das Django-ORM ist intelligent genug, um diese in einer einzigen Abfrage zu kombinieren:

Literal.objects.filter(name__in=dups)

Der zusätzliche Anruf .values('name')nach dem Annotate-Aufruf sieht etwas seltsam aus. Ohne dies schlägt die Unterabfrage fehl. Die zusätzlichen Werte bringen das ORM dazu, nur die Namensspalte für die Unterabfrage auszuwählen.

Piper Merriam
quelle
Netter Trick, leider funktioniert dies nur, wenn nur ein Wert verwendet wird (z. B. wenn sowohl 'Name' als auch 'Telefon' verwendet wurden, würde der letzte Teil nicht funktionieren).
Guival
1
Wofür ist das .order_by()?
Stefanfoulis
4
@stefanfoulis Es löscht alle bestehenden Bestellungen. Wenn Sie eine Modellsatzreihenfolge haben, wird diese Teil der SQL- GROUP BYKlausel, und das bricht die Dinge. Fand das heraus, als du mit Subquery spielst (in dem du über eine sehr ähnliche Gruppierung machst .values())
Oli
10

Versuchen Sie es mit Aggregation

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
JamesO
quelle
Ok, das gibt die entsprechende Liste der Namen an, aber ist es möglich, IDs und andere Felder gleichzeitig auszuwählen?
Dragoner
@dragoon - nein, aber Chris Pratt hat die Alternative in seiner Antwort behandelt.
JamesO
5

Wenn Sie PostgreSQL verwenden, können Sie Folgendes tun:

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

Dies führt zu dieser ziemlich einfachen SQL-Abfrage:

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

Wenn Sie nur eine Namensliste, aber keine Objekte erstellen möchten, können Sie die folgende Abfrage verwenden

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