Django hanya memilih baris dengan nilai bidang duplikat

96

misalkan kita memiliki model dalam django yang didefinisikan sebagai berikut:

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

Bidang nama tidak unik, sehingga dapat memiliki nilai duplikat. Saya perlu menyelesaikan tugas berikut: Pilih semua baris dari model yang memiliki setidaknya satu nilai duplikat dari namebidang tersebut.

Saya tahu cara melakukannya menggunakan SQL biasa (mungkin bukan solusi terbaik):

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

Jadi, apakah mungkin untuk memilih ini menggunakan django ORM? Atau solusi SQL yang lebih baik?

memaksa
sumber

Jawaban:

193

Mencoba:

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

Ini sedekat yang bisa Anda dapatkan dengan Django. Masalahnya adalah ini akan mengembalikan a ValuesQuerySetdengan saja namedan count. Namun, Anda kemudian dapat menggunakan ini untuk membuat reguler QuerySetdengan memasukkannya kembali ke kueri lain:

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
sumber
5
Mungkin yang Anda maksud Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragoon
Kueri asli memberikanCannot resolve keyword 'id_count' into field
dragoon
2
Terima kasih atas jawaban yang diperbarui, saya pikir saya akan tetap menggunakan solusi ini, Anda bahkan dapat melakukannya tanpa pemahaman daftar dengan menggunakanvalues_list('name', flat=True)
dragoon
1
Django sebelumnya mempunyai bug ini (mungkin telah diperbaiki dalam versi terbaru) di mana jika Anda tidak menetapkan nama field untuk Countpenjelasan untuk disimpan sebagai, defaultnya adalah [field]__count. Bagaimanapun, sintaks garis bawah ganda itu juga bagaimana Django menafsirkan anda ingin melakukan penggabungan. Jadi, pada dasarnya ketika Anda mencoba memfilter itu, Django mengira Anda sedang mencoba melakukan penggabungan countyang jelas-jelas tidak ada. Cara mengatasinya adalah menentukan nama untuk hasil anotasi Anda, yaitu annotate(mycount=Count('id'))dan kemudian memfilternya mycount.
Chris Pratt
1
jika Anda menambahkan panggilan lain ke values('name')setelah panggilan Anda untuk membuat anotasi, Anda dapat menghapus pemahaman daftar dan mengatakan Literal.objects.filter(name__in=dupes)yang akan memungkinkan semua ini dijalankan dalam satu kueri.
Piper Merriam
43

Ini ditolak sebagai edit. Jadi inilah jawaban yang lebih baik

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

Ini akan mengembalikan a ValuesQuerySetdengan semua nama duplikat. Namun, Anda kemudian dapat menggunakan ini untuk membuat reguler QuerySetdengan memasukkannya kembali ke kueri lain. Django ORM cukup pintar untuk menggabungkan ini menjadi satu kueri:

Literal.objects.filter(name__in=dups)

Panggilan ekstra ke .values('name')setelah panggilan anotasi terlihat sedikit aneh. Tanpa ini, subkueri gagal. Nilai ekstra menipu ORM agar hanya memilih kolom nama untuk subkueri.

Piper Merriam
sumber
Trik yang bagus, sayangnya ini hanya akan berfungsi jika hanya satu nilai yang digunakan (misalnya jika 'nama' dan 'telepon' digunakan, bagian terakhir tidak akan berfungsi).
guival
1
Apa .order_by()untuk?
stefanfoulis
4
@stefanfoulis Ini menghapus semua pemesanan yang ada. Jika Anda memiliki urutan model-set, ini menjadi bagian dari GROUP BYklausa SQL , dan itu merusak banyak hal. Mengetahui hal itu saat bermain dengan Subquery (di mana Anda melakukan pengelompokan yang sangat mirip melalui .values())
Oli
10

coba gunakan agregasi

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
JamesO
sumber
Oke, itu memberikan daftar nama yang benar, tetapi apakah mungkin untuk memilih id dan bidang lain pada saat yang sama?
dragoon
@dragoon - tidak, tapi Chris Pratt telah membahas alternatif dalam jawabannya.
JamesO
5

Jika Anda menggunakan PostgreSQL, Anda dapat melakukan sesuatu seperti ini:

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

Ini menghasilkan kueri SQL yang agak sederhana ini:

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

Jika Anda ingin menghasilkan daftar nama saja tetapi bukan objek, Anda bisa menggunakan kueri berikut ini

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