Django에서 개수 주석에 대한 개체를 필터링하는 방법은 무엇입니까?


123

간단한 장고 모델 고려 EventParticipant:

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. 주석이 달린 값만 있으면됩니다.

문서예제는 여기에서 작동하지 않습니다 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에는 새로운 조건부 집계 기능이 있습니다. 아래 에서 허용되는 답변을 참조하십시오.

답변:


105

Django 2.0의 조건부 집계 를 사용하면 과거에 있었던 faff의 양을 더 줄일 수 있습니다. 이것은 또한 filter합계 사례보다 다소 빠른 Postgres의 논리를 사용할 것입니다 (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 (Subqueries) 방식과 Django 1.8 방식을 찾을 수 있습니다.
Ryan Castner

2
이 1.9를 예, 장고 <2 시도,이 경우주의 예외없이 실행되지만 필터가 단순히 적용되지 않습니다. 따라서 Django <2에서 작동하는 것처럼 보일 수 있지만 그렇지 않습니다.
djvg

여러 필터를 추가해야하는 경우 Q () 인수에으로 구분하여 추가 할 수 있습니다 (예 : filter = Q (participants__is_paid = True, somethingelse = value)
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가 묻는 질문은 이것이 작동할지 여부보다 큰 세트에 비효율적인지 여부입니다 .... 맞습니까? 알아야 할 가장 중요한 것은 파이썬에서하는 것이 아니라 SQL case 절을 ​​생성 한다는 것입니다.- github.com / django / django / blob / master / django / db / models / …를 참조하십시오 . 간단한 예 조인보다 더 나은 것,하지만 더 복잡한 버전은 하위 쿼리 등을 포함 할 수있다
헤이든 크로커

1
이것을 Count(대신 Sum) 와 함께 사용할 때 ( default=Nonedjango 2 filter인수를 사용하지 않는 경우) 설정해야한다고 생각합니다 .
djvg

41

최신 정보

내가 언급 한 하위 쿼리 접근 방식은 이제 subquery-expressions 를 통해 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()
    )
)

최적화 (적절한 인덱싱 사용) 가 더 빠르고 쉬워야하므로 집계 (sum + case) 보다 이것을 선호합니다 .

이전 버전의 경우 다음을 사용하여 동일한 결과를 얻을 수 있습니다. .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! .extraDjango에서 SQL을 피하는 것을 선호하기 때문에을 사용하지 않고 방법을 찾은 것 같습니다. :) 질문을 업데이트하겠습니다.
rudyryk 2015-06-10

1
천만에요, btw 저는이 접근 방식을 알고 있지만 지금까지 작동하지 않는 솔루션 이었기 때문에 그것에 대해 언급하지 않았습니다. 그러나 방금에서 수정되었다는 것을 알았 Django 1.8.2으므로 해당 버전을 사용하고있는 것 같습니다. 여기여기
Todor

2
0이어야 할 때 None이 생성된다는 것을 알고 있습니다. 다른 사람이 이것을 얻습니까?
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. Participant그들에게 s를 추가하십시오 .

    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별로 모든 s 그룹화 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있는 것은 Participant요소별로 그룹화 된 두 개의 버킷을 만드는 것 event입니다. 해당 버킷에는 Participant.

  4. 그런 다음 원본 세트가 포함 된 버킷에 주석을 달 수 있습니다 Participant. 여기서 우리는의 수를 계산하려고합니다 Participant. 이것은 단순히 id해당 버킷에있는 요소 의 s를 계산하여 수행됩니다 (이니까 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.