Bagaimana cara menyaring objek untuk penjelasan hitungan di Django?

123

Pertimbangkan model Django sederhana Eventdan Participant:

class Event(models.Model):
    title = models.CharField(max_length=100)

class Participant(models.Model):
    event = models.ForeignKey(Event, db_index=True)
    is_paid = models.BooleanField(default=False, db_index=True)

Sangat mudah untuk membuat anotasi kueri acara dengan jumlah total peserta:

events = Event.objects.all().annotate(participants=models.Count('participant'))

Bagaimana cara membuat anotasi dengan jumlah peserta yang difilter is_paid=True?

Saya perlu menanyakan semua acara terlepas dari jumlah pesertanya, misalnya, saya tidak perlu memfilter berdasarkan hasil yang dianotasi. Kalau ada 0peserta tidak apa-apa, saya hanya perlu 0di annotated value.

The contoh dari dokumentasi tidak bekerja di sini, karena tidak termasuk benda dari query bukannya annotating mereka dengan 0.

Memperbarui. Django 1.8 mempunyai fitur ekspresi kondisional baru , jadi sekarang kita bisa melakukan seperti ini:

events = Event.objects.all().annotate(paid_participants=models.Sum(
    models.Case(
        models.When(participant__is_paid=True, then=1),
        default=0,
        output_field=models.IntegerField()
    )))

Pembaruan 2. Django 2.0 mempunyai fitur agregasi Bersyarat baru , lihat jawaban yang diterima di bawah.

Rudyryk
sumber

Jawaban:

105

Agregasi bersyarat di Django 2.0 membolehkan anda untuk lebih mengurangi jumlah faff ini di masa lalu. Ini juga akan menggunakan filterlogika Postgres , yang agak lebih cepat daripada kasus penjumlahan (saya telah melihat angka seperti 20-30% diputar-putar).

Bagaimanapun, dalam kasus Anda, kami melihat sesuatu yang sederhana seperti:

from django.db.models import Q, Count
events = Event.objects.annotate(
    paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)

Ada bagian terpisah di dokumen tentang pemfilteran pada anotasi . Ini sama dengan agregasi bersyarat tetapi lebih seperti contoh saya di atas. Bagaimanapun, ini jauh lebih sehat daripada subkueri gnarly yang saya lakukan sebelumnya.

Oli
sumber
BTW, tidak ada contoh seperti itu di tautan dokumentasi, hanya aggregatepenggunaan yang ditampilkan. Apakah Anda sudah menguji pertanyaan seperti itu? (Saya belum dan saya ingin percaya! :)
rudyryk
2
Saya sudah. Mereka bekerja. Saya benar-benar mencapai tambalan aneh di mana subkueri lama (super rumit) berhenti bekerja setelah memutakhirkan ke Django 2.0 dan saya berhasil menggantinya dengan hitungan tersaring yang sangat sederhana. Ada contoh dalam dokumen yang lebih baik untuk anotasi jadi saya akan menariknya sekarang.
Oli
1
Ada beberapa jawaban di sini, ini adalah cara Django 2.0, dan di bawah ini Anda akan menemukan cara Django 1.11 (Subkueri), dan cara Django 1.8.
Ryan Castner
2
Hati-hati, jika Anda mencoba ini di Django <2, misalnya 1.9, ini akan berjalan tanpa terkecuali, tetapi filter tidak diterapkan. Jadi ini mungkin tampak bekerja dengan Django <2, tapi tidak.
djvg
Jika Anda perlu menambahkan beberapa filter, Anda dapat menambahkannya dalam argumen Q () dengan dipisahkan oleh, sebagai contoh filter = Q (particip__is_paid = True, sesuatuelse = nilai)
Tobit
93

Baru saja menemukan bahwa Django 1.8 mempunyai fitur ekspresi kondisional baru , jadi sekarang kita bisa melakukan seperti ini:

events = Event.objects.all().annotate(paid_participants=models.Sum(
    models.Case(
        models.When(participant__is_paid=True, then=1),
        default=0, output_field=models.IntegerField()
    )))
Rudyryk
sumber
Apakah ini solusi yang memenuhi syarat bila item yang cocok banyak? Katakanlah saya ingin menghitung peristiwa klik yang terjadi pada minggu terakhir.
SverkerSbrg
Kenapa tidak? Maksud saya, mengapa kasus Anda berbeda? Dalam kasus di atas, mungkin ada sejumlah peserta yang dibayar pada acara tersebut.
rudyryk
Saya pikir pertanyaan yang ditanyakan @SverkerSbrg adalah apakah ini tidak efisien untuk set besar, daripada apakah itu akan berhasil atau tidak .... benar? Yang paling penting untuk diketahui adalah bahwa itu tidak melakukannya dengan python, itu membuat klausa kasus SQL - lihat github.com/django/django/blob/master/django/db/models/… - jadi itu akan berkinerja cukup, contoh sederhana akan lebih baik daripada bergabung, tetapi versi yang lebih kompleks dapat menyertakan subkueri dll.
Hayden Crocker
1
Saat menggunakan ini dengan Count(daripada Sum) saya rasa kita harus mengatur default=None(jika tidak menggunakan filterargumen django 2 ).
djvg
41

MEMPERBARUI

Pendekatan sub-kueri yang saya sebutkan sekarang didukung di Django 1.11 melalui sub-ekspresi-subkueri .

Event.objects.annotate(
    num_paid_participants=Subquery(
        Participant.objects.filter(
            is_paid=True,
            event=OuterRef('pk')
        ).values('event')
        .annotate(cnt=Count('pk'))
        .values('cnt'),
        output_field=models.IntegerField()
    )
)

Saya lebih suka ini daripada agregasi (jumlah + kasus) , karena itu harus lebih cepat dan lebih mudah untuk dioptimalkan (dengan pengindeksan yang tepat) .

Untuk versi yang lebih lama, hal yang sama dapat dicapai dengan menggunakan .extra

Event.objects.extra(select={'num_paid_participants': "\
    SELECT COUNT(*) \
    FROM `myapp_participant` \
    WHERE `myapp_participant`.`is_paid` = 1 AND \
            `myapp_participant`.`event_id` = `myapp_event`.`id`"
})
Todor
sumber
Terima kasih Todor! Sepertinya saya telah menemukan cara tanpa menggunakan .extra, karena saya lebih suka menghindari SQL di Django :) Saya akan memperbarui pertanyaannya.
Rudyryk
1
Sama-sama, btw saya mengetahui pendekatan ini, tetapi itu adalah solusi yang tidak berfungsi sampai sekarang, itulah mengapa saya tidak menyebutkannya. Namun saya baru saja menemukan bahwa itu telah diperbaiki Django 1.8.2, jadi saya rasa Anda menggunakan versi itu dan itulah mengapa ini bekerja untuk Anda. Anda dapat membaca lebih lanjut tentang itu di sini dan di sini
Todor
2
Saya mendapatkan bahwa ini menghasilkan None padahal seharusnya 0. Adakah yang mendapatkan ini?
StefanJCollier
@StefanJCollier Ya, saya Nonejuga mengerti . Solusi saya adalah menggunakan Coalesce( from django.db.models.functions import Coalesce). Anda menggunakannya seperti ini: Coalesce(Subquery(...), 0). Mungkin ada pendekatan yang lebih baik.
Adam Taylor
6

Saya akan menyarankan untuk menggunakan .valuesmetode Participantqueryset Anda sebagai gantinya.

Singkatnya, apa yang ingin Anda lakukan diberikan oleh:

Participant.objects\
    .filter(is_paid=True)\
    .values('event')\
    .distinct()\
    .annotate(models.Count('id'))

Contoh lengkapnya adalah sebagai berikut:

  1. Buat 2 Eventd:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
  2. Tambahkan Participantke mereka:

    part1l = [Participant.objects.create(event=event1, is_paid=((_%2) == 0))\
              for _ in range(10)]
    part2l = [Participant.objects.create(event=event2, is_paid=((_%2) == 0))\
              for _ in range(50)]
  3. Kelompokkan semua Participantmenurut eventbidangnya:

    Participant.objects.values('event')
    > <QuerySet [{'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, '...(remaining elements truncated)...']>

    Di sini diperlukan perbedaan:

    Participant.objects.values('event').distinct()
    > <QuerySet [{'event': 1}, {'event': 2}]>

    Apa .valuesdan yang .distinctsedang dilakukan di sini adalah bahwa mereka membuat dua Participantkelompok yang dikelompokkan berdasarkan elemennya event. Perhatikan bahwa ember itu berisi Participant.

  4. Anda kemudian dapat memberi anotasi pada keranjang tersebut karena keranjang berisi kumpulan aslinya Participant. Di sini kami ingin menghitung jumlahnya Participant, ini hanya dilakukan dengan menghitung ids dari elemen dalam kotak tersebut (karena itu adalah Participant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
  5. Terakhir, Anda hanya menginginkan Participantdengan is_paidmakhluk True, Anda dapat menambahkan filter di depan ekspresi sebelumnya, dan ini menghasilkan ekspresi yang ditunjukkan di atas:

    Participant.objects\
        .filter(is_paid=True)\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 5}, {'event': 2, 'id__count': 25}]>

Satu-satunya kelemahan adalah Anda harus mengambil Eventsetelahnya karena Anda hanya memiliki iddari metode di atas.

Raffi
sumber
2

Hasil apa yang saya cari:

  • Orang (penerima tugas) yang memiliki tugas yang ditambahkan ke laporan. - Jumlah Unik Total Orang
  • Orang yang memiliki tugas ditambahkan ke laporan, tetapi untuk tugas yang kemampuan penagihannya lebih dari 0 saja.

Secara umum, saya harus menggunakan dua kueri berbeda:

Task.objects.filter(billable_efforts__gt=0)
Task.objects.all()

Tapi saya ingin keduanya dalam satu kueri. Karenanya:

Task.objects.values('report__title').annotate(withMoreThanZero=Count('assignee', distinct=True, filter=Q(billable_efforts__gt=0))).annotate(totalUniqueAssignee=Count('assignee', distinct=True))

Hasil:

<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>
Arindam Roychowdhury
sumber