Django'da sayım açıklaması için nesneleri nasıl filtreleyebilirim?


123

Basit Django modellerini düşünün Eventve 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)

Toplam katılımcı sayısı ile etkinlikler sorgusuna açıklama eklemek kolaydır:

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

Filtrelenen katılımcı sayısı ile nasıl açıklama yapılır is_paid=True?

Katılımcı sayısından bağımsız olarak tüm olayları sorgulamalıyım , örneğin açıklamalı sonuca göre filtrelememe gerek yok. 0Katılımcı varsa , sorun değil, sadece 0açıklamalı değere ihtiyacım var .

Belgelere örnek hariç tutan onları annotating yerine sorgudan nesneleri, çünkü burada çalışmıyor 0.

Güncelleme. Django 1.8 yeni koşullu ifadeler özelliğine sahiptir , bu yüzden şimdi şöyle yapabiliriz:

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

Güncelleme 2. Django 2.0, yeni Koşullu toplama özelliğine sahiptir, aşağıdaki kabul edilen yanıta bakın.

Yanıtlar:


105

Django 2.0'daki koşullu toplama , geçmişte olan faff miktarını daha da azaltmanıza olanak tanır. Bu aynı zamanda filterbir toplam durumdan biraz daha hızlı olan Postgres mantığını da kullanacaktır (etrafta% 20-30 bantlanmış sayılar gördüm).

Her neyse, sizin durumunuzda, şu kadar basit bir şeye bakıyoruz:

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

Belgelerde ek açıklamaları filtrelemeyle ilgili ayrı bir bölüm var . Koşullu toplama ile aynı şeyler ama daha çok yukarıdaki örneğime benziyor. Her iki durumda da, bu daha önce yaptığım garip alt sorgulardan çok daha sağlıklı.


BTW, dokümantasyon bağlantısında böyle bir örnek yok, sadece aggregatekullanım gösteriliyor. Bu tür sorguları zaten test ettiniz mi? (
Yapmadım

2
Sahibim. Çalışırlar. Aslında, eski (süper karmaşık) bir alt sorgunun Django 2.0'a yükselttikten sonra çalışmayı bıraktığı ve onu süper basit bir filtrelenmiş sayıyla değiştirmeyi başardığı garip bir yamaya çarptım. Ek açıklamalar için daha iyi bir doküman içi örnek var, bu yüzden şimdi onu inceleyeceğim.
Oli

1
Burada birkaç cevap var, bu Django 2.0 yolu ve aşağıda Django 1.11 (Alt sorgular) yolunu ve Django 1.8 yolunu bulacaksınız.
Ryan Castner

2
Bu 1.9 örneğin, Django <2'de denemek, bu takdirde, dikkat edecek istisnasız çalıştırabilir, ancak filtre basitçe uygulanmaz. Dolayısıyla Django <2 ile çalışıyor gibi görünebilir, ancak çalışmaz.
djvg

Birden fazla filtre eklemeniz gerekiyorsa, bunları Q () bağımsız değişkenine ayırarak ekleyebilirsiniz, örneğin filtre = Q (katılımcılar__is_paid = Doğru, başka bir şey = değer)
Tobit

93

Az önce Django 1.8'in yeni koşullu ifadeler özelliğine sahip olduğunu keşfettim , bu yüzden şimdi şöyle yapabiliriz:

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

Eşleşen öğeler çok olduğunda bu uygun bir çözüm müdür? Geçen hafta meydana gelen tıklama etkinliklerini saymak istediğimi varsayalım.
SverkerSbrg

Neden olmasın? Demek istediğim, davan neden farklı? Yukarıdaki durumda, etkinliğe herhangi bir sayıda ücretli katılımcı olabilir.
rudyryk

Sanırım @SverkerSbrg'ın sorduğu soru, işe yarayıp yaramayacağından ziyade bunun büyük kümeler için verimsiz olup olmadığıdır .... doğru mu? Bilmeniz gereken en önemli şey, bunu python'da yapmadığıdır, bir SQL durum cümlesi oluşturmasıdır - bkz. Github.com/django/django/blob/master/django/db/models/… - bu nedenle makul bir performans sergileyecektir, basit bir örnek, bir birleşimden daha iyi olurdu, ancak daha karmaşık sürümler alt sorgular vb. içerebilir.
Hayden Crocker 18'17

1
Bunu Count(yerine Sum) ile kullanırken, sanırım ayarlamalıyız default=None(django 2 filterargümanını kullanmıyorsanız ).
djvg

41

GÜNCELLEME

Bahsettiğim alt sorgu yaklaşımı artık alt sorgu ifadeleri aracılığıyla Django 1.11'de destekleniyor .

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

Bunu toplamaya (toplam + büyük / küçük harf) tercih ediyorum , çünkü optimize edilmesi daha hızlı ve daha kolay olmalı (uygun indeksleme ile) .

Daha eski sürüm için, aynı şey kullanılarak elde edilebilir .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`"
})

Teşekkürler Todor! Görünüşe göre kullanmadan yolu buldum .extra, çünkü Django'da SQL'den kaçınmayı tercih ediyorum :) Soruyu güncelleyeceğim.
rudyryk

1
Rica ederim, ama bu yaklaşımın farkındayım ama şimdiye kadar işe yaramayan bir çözümdü, bu yüzden bundan bahsetmedim. Ancak bunun düzeltildiğini yeni buldum, bu Django 1.8.2yüzden sanırım o sürümdesiniz ve bu yüzden sizin için çalışıyor. Bununla ilgili daha fazla bilgiyi burada ve burada
Todor

2
Bunun 0 olması gerektiğinde hiçbiri üretmediğini anlıyorum. Bunu alan başka biri var mı?
StefanJCollier

@StefanJCollier Evet, bende de var None. Benim çözümüm Coalesce( from django.db.models.functions import Coalesce) kullanmaktı . Böyle kullanabilirsiniz: Coalesce(Subquery(...), 0). Yine de daha iyi bir yaklaşım olabilir.
Adam Taylor

6

Onun yerine sorgu kümenizin .valuesyöntemini kullanmanızı öneririm Participant.

Kısacası, yapmak istediğiniz şey şudur:

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

Tam bir örnek aşağıdaki gibidir:

  1. 2 Events oluşturun :

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
    
  2. ParticipantOnlara s ekleyin :

    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. Tüm Participante'leri alanlarına göre eventgrupla:

    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)...']>
    

    Burada farklılığa ihtiyaç vardır:

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

    Ne .valuesve .distinctburada yapıyoruz onlar iki grupları oluştururken olmasıdır Participantonların elemanı göre gruplandırılmış s event. Bu kovaların içerdiğini unutmayın Participant.

  4. Daha sonra, orijinal setini içerdikleri için bu paketlere açıklama ekleyebilirsiniz Participant. Burada sayısını saymak istiyoruz Participant, bu basitçe idbu kovalardaki öğelerin s'lerini sayarak yapılır (çünkü bunlar Participant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
    
  5. Son olarak , sadece Participantbir is_paidvarlıkla istediğinizde True, önceki ifadenin önüne bir filtre ekleyebilirsiniz ve bu, yukarıda gösterilen ifadeyi verir:

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

Tek dezavantajı, Eventsadece idyukarıdaki yöntemden sahip olduğunuz için sonrasını geri almanız gerektiğidir .


2

Hangi sonucu arıyorum:

  • Rapora eklenmiş görevleri olan kişiler (atanan). - Toplam Benzersiz Kişi Sayısı
  • Rapora eklenmiş görevleri olan, ancak faturalandırılabilirliği yalnızca 0'dan fazla olan kişiler.

Genel olarak, iki farklı sorgu kullanmam gerekir:

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

Ama ikisini de tek bir sorguda istiyorum. Dolayısıyla:

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

Sonuç:

<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.