Django wybiera tylko wiersze ze zduplikowanymi wartościami pól

99

załóżmy, że mamy model w django zdefiniowany w następujący sposób:

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

Pole nazwy nie jest unikalne i dlatego może mieć zduplikowane wartości. Muszę wykonać następujące zadania: Wybierz wszystkie wiersze z modelu, które mają co najmniej jeden duplikat wartości w namepolu.

Wiem jak to zrobić używając zwykłego SQL (może nie być najlepszym rozwiązaniem):

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

Czy jest więc możliwe wybranie tego za pomocą django ORM? Albo lepsze rozwiązanie SQL?

dragon
źródło

Odpowiedzi:

198

Próbować:

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

To jest tak blisko, jak to tylko możliwe dzięki Django. Problem w tym, że to zwróci ValuesQuerySettylko namei count. Możesz jednak użyć tego do skonstruowania zwykłego QuerySet, przesyłając go z powrotem do innego zapytania:

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
źródło
5
Prawdopodobnie miałeś na myśli Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragoon
Oryginalne zapytanie podajeCannot resolve keyword 'id_count' into field
dragoon
2
Dzięki za zaktualizowaną odpowiedź, myślę, że zostanę przy tym rozwiązaniu, możesz to zrobić nawet bez zrozumienia listy za pomocąvalues_list('name', flat=True)
dragoon
1
Django wcześniej miał błąd w tym (mógł zostać naprawiony w ostatnich wersjach), gdzie jeśli nie określisz nazwy pola dla Countadnotacji do zapisania jako, domyślnie jest to [field]__count. Jednak składnia podwójnego podkreślenia jest również tym, jak Django interpretuje, że chcesz wykonać złączenie. Tak więc, zasadniczo, kiedy próbujesz to filtrować, Django myśli, że próbujesz zrobić połączenie, countktóre oczywiście nie istnieje. Rozwiązaniem jest określenie nazwy wyniku adnotacji, annotate(mycount=Count('id'))a następnie włączenie filtru mycount.
Chris Pratt
1
jeśli dodasz kolejne wywołanie do values('name')po wywołaniu adnotacji, możesz usunąć rozumienie listy i powiedzieć, Literal.objects.filter(name__in=dupes)które pozwoli to wszystko wykonać w jednym zapytaniu.
Piper Merriam
45

Zostało to odrzucone jako zmiana. Więc tutaj jest to lepsza odpowiedź

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

Zwróci to ValuesQuerySetze wszystkimi zduplikowanymi nazwami. Możesz jednak użyć tego do skonstruowania zwykłego QuerySet, przesyłając go z powrotem do innego zapytania. ORM django jest wystarczająco inteligentny, aby połączyć je w jedno zapytanie:

Literal.objects.filter(name__in=dups)

Dodatkowe wywołanie .values('name')po wywołaniu adnotacji wygląda trochę dziwnie. Bez tego podzapytanie kończy się niepowodzeniem. Dodatkowe wartości sprawiają, że ORM wybiera tylko kolumnę nazwy dla podzapytania.

Piper Merriam
źródło
Niezła sztuczka, niestety zadziała to tylko wtedy, gdy zostanie użyta tylko jedna wartość (np. Jeśli użyto zarówno „name”, jak i „phone”, ostatnia część nie zadziała).
guival
1
Do czego służy .order_by()?
stefanfoulis
4
@stefanfoulis Usuwa wszelkie istniejące zamówienia. Jeśli masz porządkowanie zestawu modeli, staje się to częścią GROUP BYklauzuli SQL , a to psuje. Odkryłem to, grając z Subquery (w którym robisz bardzo podobne grupowanie przez .values())
Oli
10

spróbuj użyć agregacji

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
JamesO
źródło
Ok, to daje aktualną listę nazw, ale czy można jednocześnie wybrać identyfikatory i inne pola?
dragoon
@dragoon - nie, ale Chris Pratt omówił alternatywę w swojej odpowiedzi.
JamesO
5

Jeśli używasz PostgreSQL, możesz zrobić coś takiego:

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

Wynika z tego raczej proste zapytanie SQL:

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
źródło
0

Jeśli chcesz uzyskać tylko listę nazw, ale nie obiekty, możesz użyć następującego zapytania

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
user2959723
źródło