web-dev-qa-db-ja.com

Djangoでカウント注釈のオブジェクトをフィルタリングするにはどうすればよいですか?

単純な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が必要です。

ドキュメントの例 は、オブジェクトを0で注釈するのではなくクエリから除外するため、ここでは機能しません。

Update. 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には新しい 条件付き集計 機能があります。下記の 受け入れられた回答 を参照してください。

103
rudyryk

条件付き集計 in Django 2.0を使用すると、これまでにあった無駄の量をさらに減らすことができます。これには、Postgresのfilterロジックも使用します。これは、合計の場合よりもやや高速です(20〜30%ほどの数字が見られます)。

とにかく、あなたの場合、私たちは次のような単純なものを見ています:

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

ドキュメントには、 アノテーションのフィルタリング についての別のセクションがあります。これは条件付き集計と同じものですが、上記の例に似ています。いずれにせよ、これは私が以前行っていた厄介なサブクエリよりもずっと健康的です。

63
Oli

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

UPDATE

言及したサブクエリアプローチは、 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`"
})
38
Todor

代わりにParticipantクエリセットの .values メソッドを使用することをお勧めします。

簡単に言えば、あなたがしたいことは以下によって与えられます:

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

完全な例は次のとおりです。

  1. 2つのEventsを作成します。

    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. すべてのParticipanteventフィールドでグループ化します。

    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でグループ化されたeventsの2つのバケットを作成していることです。これらのバケットにはParticipantが含まれていることに注意してください。

  4. 元のParticipantのセットが含まれているため、これらのバケットに注釈を付けることができます。ここで、Participantの数をカウントします。これは、これらのバケット内の要素のidsをカウントすることで簡単に実行できます(これらはParticipantであるため)。

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
    
  5. 最後に、ParticipantのみでTrueであるis_paidのみが必要な場合は、前の式の前にフィルターを追加するだけで、上記の式が得られます。

    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を取得する必要があることです。

4
Raffi

私が探している結果:

  • レポートにタスクを追加した人(担当者)。 -一意の合計人数
  • レポートにタスクが追加されているが、タスクwhoeの請求能力は0を超える場合のみ。

一般的に、2つの異なるクエリを使用する必要があります:

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

しかし、私は両方を1つのクエリに含める必要があります。したがって:

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}]>