Djangoでcountアノテーションのオブジェクトをフィルタリングする方法は?


123

単純なDjangoモデル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()
    )))

Update 2. Django 2.0には新しい条件付き集約機能があります。以下の承認済みの回答を参照してください。

回答:


104

Django 2.0の条件付き集約により、これまでに発生した問題の量をさらに減らすことができます。これは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))
)

アノテーションのフィルタリングに関するドキュメントには、別のセクションがあります。条件付き集計と同じですが、上記の例のようになっています。どちらにせよ、これは私が以前行っていた危険なサブクエリよりもはるかに健全です。


ところで、ドキュメントリンクにはそのような例はありませんaggregate。使用法のみが表示されます。そのようなクエリをすでにテストしましたか?(私は信じていません!:)
rudyryk

2
私が持っています。彼らが働きます。Django 2.0にアップグレードした後、古い(非常に複雑な)サブクエリが機能しなくなった奇妙なパッチに実際に遭遇し、それをなんとかして非常に単純なフィルターカウントに置き換えました。アノテーションのより優れたドキュメント内の例があるので、ここでそれを引き込みます。
Oli

1
ここにいくつかの答えがあります。これはDjango 2.0の方法であり、その下にDjango 1.11(サブクエリ)の方法と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 2017年

何故なの?つまり、あなたのケースはなぜ違うのですか?上記の場合、イベントには有料の参加者がいくつでも参加できます。
rudyryk 2017

@SverkerSbrgが尋ねている質問は、これが機能するかどうかではなく、大規模なセットに対して非効率であるかどうかです...正しいですか?知っておくべき最も重要なことは、それがpythonで実行していないこと、SQLのcase句を作成していることです-github.com/django/django/blob/master/django/db/models/…を参照してください。単純な例は結合よりも優れていますが、より複雑なバージョンにはサブクエリなどを含めることができます
Hayden Crocker

1
これをCount(の代わりにSum)でdefault=None使用する場合、設定する必要があると思います(django 2 filter引数を使用しない場合)。
djvg

41

更新

私が言及しているサブクエリのアプローチは、サブクエリ式を介して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`"
})

トドール、ありがとう!.extraDjangoではSQLを避けたいので、を使用せずに方法を見つけたようです:)質問を更新します。
rudyryk 2015年

1
私はこのアプローチを知っていますが、大歓迎ですが、これは今まで機能しないソリューションでした。そのため、私はそれについて言及しませんでした。しかし、私はそれがで修正されていることを見つけたDjango 1.8.2ので、あなたはそのバージョンを使用していると思います、それがそれがあなたのために働く理由です。詳細については、こちらこちらをご覧ください
Todor

2
これは0でなければならないときにNoneを生成することがわかります。他の誰かがこれを取得していますか?
StefanJCollier 2018

@StefanJCollierはい、私も得Noneました。私の解決策はCoalescefrom django.db.models.functions import Coalesce)を使用することでした。次のように使用しますCoalesce(Subquery(...), 0)。しかし、より良いアプローチがあるかもしれません。
アダムテイラー

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のをグループ化し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、要素によってグループ化されたの2つのバケットを作成していること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. 最後にParticipantis_paidbeing のみが必要です。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より大きいタスクのみ。

一般に、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}]>
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.