Bagaimana cara kueri sebagai GROUP BY di Django?

333

Saya meminta model:

Members.objects.all()

Dan itu kembali:

Eric, Salesman, X-Shop
Freddie, Manager, X2-Shop
Teddy, Salesman, X2-Shop
Sean, Manager, X2-Shop

Apa yang saya inginkan adalah mengetahui cara Django terbaik untuk mem- group_byburn query ke database saya, seperti:

Members.objects.all().group_by('designation')

Tentu saja itu tidak berhasil. Saya tahu kita bisa melakukan beberapa trik django/db/models/query.py, tetapi saya hanya ingin tahu bagaimana melakukannya tanpa menambal.

cukup keras
sumber

Jawaban:

484

Jika Anda bermaksud melakukan agregasi, Anda dapat menggunakan fitur agregasi dari ORM :

from django.db.models import Count
Members.objects.values('designation').annotate(dcount=Count('designation'))

Ini menghasilkan permintaan yang mirip dengan

SELECT designation, COUNT(designation) AS dcount
FROM members GROUP BY designation

dan hasilnya akan berupa

[{'designation': 'Salesman', 'dcount': 2}, 
 {'designation': 'Manager', 'dcount': 2}]
Guðmundur H
sumber
6
@ Harry: Anda bisa rantai itu. Sesuatu seperti:Members.objects.filter(date=some_date).values('designation').annotate(dcount=Count('designation'))
Eli
57
saya punya pertanyaan, permintaan ini hanya mengembalikan penunjukan dan dcount, bagaimana jika saya ingin mendapatkan nilai lain dari tabel juga?
AJ
19
Perhatikan bahwa jika pengurutan Anda adalah bidang selain penunjukan, itu tidak akan berfungsi tanpa mengatur ulang pengurutan. Lihat stackoverflow.com/a/1341667/202137
Gidgidonihah
12
@ Gidgidonihah Benar, contohnya harus dibacaMembers.objects.order_by('disignation').values('designation').annotate(dcount=Count('designation'))
bjunix
7
saya punya pertanyaan, permintaan ini hanya mengembalikan penunjukan dan dcount, bagaimana jika saya ingin mendapatkan nilai lain dari tabel juga?
Yann 叶
55

Solusi mudah, tetapi bukan cara yang tepat adalah dengan menggunakan SQL mentah :

results = Members.objects.raw('SELECT * FROM myapp_members GROUP BY designation')

Solusi lain adalah dengan menggunakan group_byproperti:

query = Members.objects.all().query
query.group_by = ['designation']
results = QuerySet(query=query, model=Members)

Anda sekarang dapat mengulangi variabel hasil untuk mengambil hasil Anda. Catatan yang group_bytidak didokumentasikan dan dapat diubah dalam versi Django yang akan datang.

Dan ... mengapa Anda ingin menggunakannya group_by? Jika Anda tidak menggunakan agregasi, Anda dapat menggunakan order_byuntuk mencapai hasil yang sama.

Michael
sumber
Bisakah Anda memberi tahu saya cara melakukannya menggunakan order_by ??
simplyharsh
2
Hai, jika Anda tidak menggunakan agregasi, Anda dapat meniru group_by dengan menggunakan order_by dan menghilangkan entri yang tidak Anda butuhkan. Tentu saja, ini adalah emulasi dan hanya bisa digunakan ketika menggunakan tidak banyak data. Karena dia tidak berbicara tentang agregasi, saya pikir itu bisa menjadi solusi.
Michael
Hai ini bagus - bisa tolong jelaskan bagaimana cara menggunakan execute_sql tampaknya tidak berfungsi ..
rh0dium
8
Perhatikan ini tidak lagi berfungsi pada Django 1.9. stackoverflow.com/questions/35558120/…
grokpot
1
Ini semacam cara hack-ish untuk menggunakan ORM. Anda tidak harus membuat instance queryset baru yang melewati queryset secara manual.
Ian Kirkpatrick
32

Anda juga dapat menggunakan regrouptag template untuk dikelompokkan berdasarkan atribut. Dari dokumen:

cities = [
    {'name': 'Mumbai', 'population': '19,000,000', 'country': 'India'},
    {'name': 'Calcutta', 'population': '15,000,000', 'country': 'India'},
    {'name': 'New York', 'population': '20,000,000', 'country': 'USA'},
    {'name': 'Chicago', 'population': '7,000,000', 'country': 'USA'},
    {'name': 'Tokyo', 'population': '33,000,000', 'country': 'Japan'},
]

...

{% regroup cities by country as country_list %}

<ul>
    {% for country in country_list %}
        <li>{{ country.grouper }}
            <ul>
            {% for city in country.list %}
                <li>{{ city.name }}: {{ city.population }}</li>
            {% endfor %}
            </ul>
        </li>
    {% endfor %}
</ul>

Terlihat seperti ini:

  • India
    • Mumbai: 19.000.000
    • Kalkuta: 15.000.000
  • Amerika Serikat
    • New York: 20.000.000
    • Chicago: 7.000.000
  • Jepang
    • Tokyo: 33.000.000

Ini juga berfungsi pada QuerySets saya percaya.

sumber: https://docs.djangoproject.com/en/2.1/ref/templates/builtins/#regroup

sunting: perhatikan bahwa regrouptag tidak berfungsi seperti yang Anda harapkan jika daftar kamus Anda tidak diurutkan kunci. Ini bekerja berulang. Jadi, sortir daftar Anda (atau set kueri) dengan kunci kerapu sebelum meneruskannya ke regrouptag.

inostia
sumber
1
Ini sempurna! Saya telah mencari banyak cara sederhana untuk melakukan ini. Dan itu bekerja pada querysets juga, itulah cara saya menggunakannya.
CarmenA
1
ini benar-benar salah jika Anda membaca dari kumpulan data yang besar dan kemudian hanya menggunakan nilai agregat.
Sławomir Lenart
@ SławomirLenart yakin, ini mungkin tidak seefisien permintaan DB langsung. Tetapi untuk kasus penggunaan sederhana ini bisa menjadi solusi yang bagus
inostia
Ini akan berfungsi jika hasilnya ditampilkan dalam templat. Tetapi, untuk JsonResponse atau respons tidak langsung lainnya. solusi ini tidak akan berfungsi.
Willy satrio nugroho
1
@Willysatrionugroho jika Anda ingin melakukannya dalam tampilan, misalnya, stackoverflow.com/questions/477820/… mungkin bekerja untuk Anda
inostia
7

Anda perlu melakukan SQL khusus seperti yang dicontohkan dalam cuplikan ini:

SQL khusus melalui subquery

Atau di manajer khusus seperti yang ditunjukkan dalam dokumen Django online:

Menambahkan metode Manajer tambahan

Van Gale
sumber
1
Jenis solusi bolak-balik. Saya akan menggunakannya, jika saya memiliki beberapa penggunaan yang panjang. Tapi di sini saya hanya perlu jumlah anggota per sebutan itu saja.
simplyharsh
Tidak masalah. Aku berpikir tentang menyebutkan 1.1 fitur agregasi tetapi membuat asumsi Anda menggunakan versi rilis :)
Van Gale
Ini semua tentang penggunaan kueri mentah, yang menunjukkan kelemahan ORM Django.
Sławomir Lenart
5

Django tidak mendukung grup gratis berdasarkan permintaan . Saya mempelajarinya dengan cara yang sangat buruk. ORM tidak dirancang untuk mendukung hal-hal seperti apa yang ingin Anda lakukan, tanpa menggunakan SQL kustom. Anda terbatas pada:

  • Sql RAW (yaitu MyModel.objects.raw ())
  • cr.execute kalimat (dan parsing buatan tangan dari hasilnya).
  • .annotate() (grup dengan kalimat dilakukan dalam model anak untuk .annotate (), dalam contoh-contoh seperti mengagregasi lines_count = Hitung ('baris'))).

Melalui queryset qsAnda dapat menelepon qs.query.group_by = ['field1', 'field2', ...]tetapi berisiko jika Anda tidak tahu permintaan apa yang Anda edit dan tidak memiliki jaminan bahwa itu akan berfungsi dan tidak merusak internal objek QuerySet. Selain itu, ini adalah API internal (tidak berdokumen) yang tidak boleh Anda akses langsung tanpa risiko kode tidak lagi kompatibel dengan versi Django yang akan datang.

Luis Masuelli
sumber
memang Anda terbatas tidak hanya dalam grup-by gratis, jadi coba SQLAlchemy bukannya Django ORM.
Sławomir Lenart
5

Ada modul yang memungkinkan Anda untuk mengelompokkan model Django dan masih bekerja dengan QuerySet di hasilnya: https://github.com/kako-nawao/django-group-by

Sebagai contoh:

from django_group_by import GroupByMixin

class BookQuerySet(QuerySet, GroupByMixin):
    pass

class Book(Model):
    title = TextField(...)
    author = ForeignKey(User, ...)
    shop = ForeignKey(Shop, ...)
    price = DecimalField(...)

class GroupedBookListView(PaginationMixin, ListView):
    template_name = 'book/books.html'
    model = Book
    paginate_by = 100

    def get_queryset(self):
        return Book.objects.group_by('title', 'author').annotate(
            shop_count=Count('shop'), price_avg=Avg('price')).order_by(
            'name', 'author').distinct()

    def get_context_data(self, **kwargs):
        return super().get_context_data(total_count=self.get_queryset().count(), **kwargs)

'book / books.html'

<ul>
{% for book in object_list %}
    <li>
        <h2>{{ book.title }}</td>
        <p>{{ book.author.last_name }}, {{ book.author.first_name }}</p>
        <p>{{ book.shop_count }}</p>
        <p>{{ book.price_avg }}</p>
    </li>
{% endfor %}
</ul>

Perbedaan dengan annotate/ aggregatebasic Django queries adalah penggunaan atribut dari bidang terkait, misalnya book.author.last_name.

Jika Anda membutuhkan PK dari instance yang telah dikelompokkan bersama, tambahkan anotasi berikut:

.annotate(pks=ArrayAgg('id'))

CATATAN: ArrayAggadalah fungsi spesifik Postgres, tersedia mulai Django 1.9 dan seterusnya: https://docs.djangoproject.com/en/1.10/ref/contrib/postgres/aggregates/#arrayagg

Risadinha
sumber
Ini Django-kelompok-oleh adalah sebuah alternatif untuk valuesmetode. Kurasa untuk tujuan yang berbeda.
LShi
1
@LShi Ini bukan alternatif untuk nilai, tentu saja tidak. valuesadalah SQL selectwhile group_byadalah SQL group by(seperti namanya ...). Mengapa downvote? Kami menggunakan kode tersebut dalam produksi untuk mengimplementasikan group_bypernyataan kompleks .
Risadinha
Doc- nya mengatakan group_by"sebagian besar berperilaku seperti metode nilai-nilai, tetapi dengan satu perbedaan ..." Doc tidak menyebutkan SQL GROUP BYdan use case yang disediakannya tidak menyarankan itu ada hubungannya dengan SQL GROUP BY. Saya akan menarik mundur suara ketika seseorang telah membuat ini jelas, tetapi dokter itu benar-benar menyesatkan.
LShi
Setelah membaca dokumen untukvalues , saya menemukan saya merindukan itu valuessendiri berfungsi seperti GROUP BY. Ini adalah kesalahanku. Saya pikir ini lebih mudah untuk digunakan itertools.groupbydaripada django-group-by ini ketika valuestidak mencukupi.
LShi
1
Tidak mungkin untuk melakukan group bydari atas dengan valuespanggilan sederhana -dengan atau tanpa annotatedan tanpa mengambil semuanya dari database. Saran Anda itertools.groupbyuntuk pekerjaan untuk dataset kecil tetapi tidak untuk beberapa ribu dataset yang mungkin ingin Anda halaman. Tentu saja, pada saat itu Anda harus memikirkan indeks pencarian khusus yang berisi data yang sudah disiapkan (sudah dikelompokkan).
Risadinha
0

The dokumen mengatakan bahwa Anda dapat menggunakan nilai-nilai kelompok queryset tersebut.

class Travel(models.Model):
    interest = models.ForeignKey(Interest)
    user = models.ForeignKey(User)
    time = models.DateTimeField(auto_now_add=True)

# Find the travel and group by the interest:

>>> Travel.objects.values('interest').annotate(Count('user'))
<QuerySet [{'interest': 5, 'user__count': 2}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited for 2 times, 
# and the interest(id=6) had only been visited for 1 time.

>>> Travel.objects.values('interest').annotate(Count('user', distinct=True)) 
<QuerySet [{'interest': 5, 'user__count': 1}, {'interest': 6, 'user__count': 1}]>
# the interest(id=5) had been visited by only one person (but this person had 
#  visited the interest for 2 times

Anda dapat menemukan semua buku dan mengelompokkannya berdasarkan nama menggunakan kode ini:

Book.objects.values('name').annotate(Count('id')).order_by() # ensure you add the order_by()

Anda dapat menonton beberapa lembar cheet di sini .

ramwin
sumber
-1

Jika saya tidak salah, Anda dapat menggunakan, apapun-query-set .group_by = [' bidang ']

Reed Jones
sumber
8
Ini tidak terjadi, setidaknya dalam Django 1.6: objek 'QuerySet' tidak memiliki atribut 'group_by'
Facundo Olano
1
Penggunaan yang tepat bisa berupa queryset.query.group_by = [...] tetapi ini akan memecah semantik kueri dan tidak berfungsi seperti yang diharapkan.
Luis Masuelli
-2
from django.db.models import Sum
Members.objects.annotate(total=Sum(designation))

pertama, Anda perlu mengimpor Sum lalu ..

Saluran YouTube Kiran S
sumber