วิธีกรองวัตถุเพื่อนับคำอธิบายประกอบใน Django


124

ลองพิจารณาโมเดล Django แบบง่ายๆEventและ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)

ง่ายต่อการใส่คำอธิบายประกอบการสืบค้นเหตุการณ์ด้วยจำนวนผู้เข้าร่วมทั้งหมด:

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

วิธีการใส่คำอธิบายประกอบกับจำนวนผู้เข้าร่วมที่กรองโดยis_paid=True?

ฉันต้องการสอบถามเหตุการณ์ทั้งหมดโดยไม่คำนึงถึงจำนวนผู้เข้าร่วมเช่นฉันไม่จำเป็นต้องกรองตามผลลัพธ์ที่มีคำอธิบายประกอบ หากมี0ผู้เข้าร่วมก็โอเคฉันแค่ต้องการ0คำอธิบายประกอบ

เช่นจากเอกสารไม่ได้ทำงานที่นี่เพราะมันไม่รวมวัตถุจากแบบสอบถามแทน annotating 0พวกเขาด้วย

ปรับปรุง Django 1.8 มีคุณสมบัตินิพจน์เงื่อนไขใหม่ดังนั้นตอนนี้เราสามารถทำได้ดังนี้:

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

อัปเดต 2. Django 2.0 มีคุณสมบัติการรวมเงื่อนไขใหม่ดูคำตอบที่ยอมรับด้านล่าง

คำตอบ:


106

การรวมตามเงื่อนไขใน Django 2.0 ช่วยให้คุณสามารถลดจำนวน faff ที่เคยมีมาในอดีตได้มากขึ้น นอกจากนี้ยังใช้filterตรรกะของ Postgres ซึ่งค่อนข้างเร็วกว่า sum-case (ฉันเคยเห็นตัวเลขเช่น 20-30% ที่พันรอบ)

อย่างไรก็ตามในกรณีของคุณเรากำลังพิจารณาสิ่งที่เรียบง่ายดังนี้:

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

มีส่วนที่แยกต่างหากในเอกสารเกี่ยวกับการกรองคำอธิบายประกอบ มันเหมือนกับการรวมแบบมีเงื่อนไข แต่เหมือนกับตัวอย่างของฉันด้านบน ไม่ว่าจะด้วยวิธีใดสิ่งนี้มีประโยชน์ต่อสุขภาพมากกว่าการค้นหาย่อยที่น่ากลัวที่ฉันเคยทำมาก่อน


BTW ไม่มีตัวอย่างดังกล่าวในลิงค์เอกสารแสดงเฉพาะaggregateการใช้งานเท่านั้น คุณได้ทดสอบคำถามดังกล่าวแล้วหรือยัง? (ฉันไม่ได้และฉันอยากจะเชื่อ! :)
rudyryk

2
ฉันมี. งานของเขา. จริงๆแล้วฉันโดนแพทช์แปลก ๆ ที่การสืบค้นย่อยเก่า (ซับซ้อนสุด ๆ ) หยุดทำงานหลังจากอัปเกรดเป็น Django 2.0 และฉันสามารถแทนที่ด้วยจำนวนที่กรองง่ายสุด ๆ มีตัวอย่างในเอกสารที่ดีกว่าสำหรับคำอธิบายประกอบดังนั้นฉันจะดึงมันมาตอนนี้
Oli

1
มีคำตอบอยู่สองสามข้อนี่คือวิธี Django 2.0 และด้านล่างคุณจะพบกับวิธี Django 1.11 (แบบสอบถามย่อย) และวิธี Django 1.8
Ryan Castner

2
ระวังถ้าคุณลองสิ่งนี้ใน Django <2 เช่น 1.9 มันจะทำงานโดยไม่มีข้อยกเว้น แต่ตัวกรองจะไม่ถูกนำไปใช้ ดังนั้นดูเหมือนว่าจะทำงานกับ Django <2 แต่ไม่ได้
djvg

หากคุณต้องการเพิ่มตัวกรองหลายตัวคุณสามารถเพิ่มได้ในอาร์กิวเมนต์ Q () โดยคั่นด้วยตัวอย่างเช่นตัวกรอง = Q (ผู้เข้าร่วม __is_paid = True, somethingelse = ค่า)
Tobit

93

เพิ่งค้นพบว่า Django 1.8 มีคุณสมบัตินิพจน์เงื่อนไขใหม่ดังนั้นตอนนี้เราสามารถทำได้ดังนี้:

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

นี่เป็นโซลูชันที่มีสิทธิ์เมื่อรายการที่ตรงกันมีจำนวนมากหรือไม่ ให้เราบอกว่าฉันต้องการนับเหตุการณ์การคลิกที่เกิดขึ้นในสัปดาห์ล่าสุด
SverkerSbrg

ทำไมจะไม่ล่ะ? ฉันหมายความว่าทำไมกรณีของคุณถึงแตกต่างกัน? ในกรณีข้างต้นอาจมีผู้เข้าร่วมกิจกรรมจำนวนเท่าใดก็ได้
rudyryk

ฉันคิดว่าคำถามที่ @SverkerSbrg ถามคือว่าสิ่งนี้ไม่มีประสิทธิภาพสำหรับชุดใหญ่มากกว่าที่จะใช้งานได้หรือไม่ .... ถูกต้อง? สิ่งสำคัญที่สุดที่ต้องรู้คือมันไม่ได้ทำใน python มันเป็นการสร้างประโยคกรณี ​​SQL - ดูgithub.com/django/django/blob/master/django/db/models/… - ดังนั้นมันจะมีประสิทธิภาพที่สมเหตุสมผล ตัวอย่างง่ายๆน่าจะดีกว่าการเข้าร่วม แต่เวอร์ชันที่ซับซ้อนกว่าอาจมีเคียวรีย่อยเป็นต้น
Hayden Crocker

1
เมื่อใช้สิ่งนี้กับCount(แทนSum) ฉันเดาว่าเราควรตั้งค่าdefault=None(ถ้าไม่ใช้filterอาร์กิวเมนต์django 2 )
djvg

41

UPDATE

วิธีแบบสอบถามย่อยที่ฉันพูดถึงได้รับการสนับสนุนในขณะนี้ใน Django 1.11 ผ่านแบบสอบถามย่อยแสดงออก

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

ฉันชอบการรวมกว่านี้(ผลรวม + กรณี)เพราะมันควรจะเร็วขึ้นและง่ายที่จะเพิ่มประสิทธิภาพ(ที่มีการจัดทำดัชนีที่เหมาะสม)

สำหรับรุ่นเก่าสามารถทำได้โดยใช้ไฟล์ .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! ดูเหมือนว่าฉันจะพบวิธีที่ไม่ต้องใช้.extraเนื่องจากฉันต้องการหลีกเลี่ยง SQL ใน Django :) ฉันจะอัปเดตคำถาม
rudyryk

1
ยินดีต้อนรับฉันรู้วิธีนี้ แต่มันเป็นวิธีแก้ปัญหาที่ไม่ได้ผลจนถึงตอนนี้นั่นคือเหตุผลที่ฉันไม่ได้พูดถึงมัน อย่างไรก็ตามฉันเพิ่งพบว่าได้รับการแก้ไขDjango 1.8.2แล้วดังนั้นฉันเดาว่าคุณใช้เวอร์ชันนั้นและนั่นเป็นเหตุผลว่าทำไมมันถึงได้ผลสำหรับคุณ คุณสามารถอ่านเพิ่มเติมได้ที่นี่และที่นี่
Todor

2
ฉันเข้าใจว่าสิ่งนี้ทำให้เกิด None เมื่อมันควรจะเป็น 0 มีใครรับสิ่งนี้อีกไหม
StefanJCollier

@StefanJCollier ใช่ฉันก็Noneเช่นกัน วิธีแก้ปัญหาของฉันคือใช้Coalesce( from django.db.models.functions import Coalesce) คุณใช้มันดังนี้: Coalesce(Subquery(...), 0). อาจมีแนวทางที่ดีกว่าแม้ว่า
Adam Taylor

6

ฉันขอแนะนำให้ใช้.valuesวิธีการParticipantสืบค้นของคุณแทน

ในระยะสั้นสิ่งที่คุณต้องการทำมีให้โดย:

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

ตัวอย่างที่สมบูรณ์มีดังต่อไปนี้:

  1. สร้าง 2 Eventวินาที:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
  2. เพิ่มParticipants ให้กับพวกเขา:

    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. จัดกลุ่มทั้งหมดParticipantตามeventฟิลด์:

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

    จำเป็นต้องมีความแตกต่างที่นี่:

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

    อะไร.valuesและ.distinctจะทำที่นี่เป็นที่ที่พวกเขาจะสร้างสองถังParticipants eventกลุ่มตามองค์ประกอบของพวกเขา Participantโปรดทราบว่าผู้ที่มีบุ้งกี๋

  4. Participantจากนั้นคุณสามารถใส่คำอธิบายประกอบบุ้งกี๋ผู้ที่พวกเขามีชุดของเดิม ที่นี่เราต้องการนับจำนวนParticipantซึ่งทำได้ง่ายๆโดยการนับidองค์ประกอบในที่เก็บข้อมูลเหล่านั้น (เนื่องจากเป็นParticipant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
  5. สุดท้ายคุณต้องการเพียงแค่Participantสิ่งมีis_paidชีวิตTrueคุณสามารถเพิ่มตัวกรองที่ด้านหน้าของนิพจน์ก่อนหน้านี้และสิ่งนี้จะได้รับนิพจน์ที่แสดงด้านบน:

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

ข้อเสียเปรียบเพียงประการเดียวคือคุณต้องดึงข้อมูลในEventภายหลังเนื่องจากคุณมีเพียงidวิธีการด้านบนเท่านั้น


2

ฉันกำลังมองหาผลลัพธ์อะไร:

  • บุคคล (ผู้รับมอบหมาย) ที่มีงานเพิ่มลงในรายงาน - จำนวนคนที่ไม่ซ้ำกันทั้งหมด
  • ผู้ที่มีงานเพิ่มลงในรายงาน แต่สำหรับงานที่มีความสามารถในการเรียกเก็บเงินมากกว่า 0 เท่านั้น

โดยทั่วไปฉันจะต้องใช้สองแบบสอบถามที่แตกต่างกัน:

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

แต่ฉันต้องการทั้งสองอย่างในแบบสอบถามเดียว ดังนั้น:

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

ผลลัพธ์:

<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.