如何在Django中过滤count对注释的对象?

rud*_*ryk 109 python django django-models django-aggregation

考虑简单的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)
Run Code Online (Sandbox Code Playgroud)

使用参与者总数来注释事件查询很容易:

events = Event.objects.all().annotate(participants=models.Count('participant'))
Run Code Online (Sandbox Code Playgroud)

如何用过滤的参与者数量进行注释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()
    )))
Run Code Online (Sandbox Code Playgroud)

更新2. Django 2.0具有新的条件聚合功能,请参阅下面接受的答案.

rud*_*ryk 92

刚刚发现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()
    )))
Run Code Online (Sandbox Code Playgroud)


Oli*_*Oli 73

Django 2.0中的条件聚合允许您进一步减少过去的faff数量.这也将使用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))
)
Run Code Online (Sandbox Code Playgroud)

文档中有一个关于过滤注释的单独部分.它与条件聚合相同,但更像我上面的例子.无论哪种方式,这比我以前做的粗糙的子查询更健康.

  • 我有.他们工作.我实际上遇到了一个奇怪的补丁,一个旧的(超级复杂的)子查询在升级到Django 2.0后停止工作,我设法用一个超级简单的过滤计数替换它.有一个更好的in-doc示例注释,所以我现在就把它拉出来. (2认同)
  • 请注意,如果您在 Django <2,例如 1.9 中尝试此操作,它*将*无一例外地运行,但根本不会应用过滤器。所以它可能看起来与 Django <2 一起工作,但事实并非如此。 (2认同)

Tod*_*dor 39

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()
    )
)
Run Code Online (Sandbox Code Playgroud)

我更喜欢这种聚合(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`"
})
Run Code Online (Sandbox Code Playgroud)

  • 我得到的是,当它应该为0时,这会产生一个无.其他人得到这个吗? (2认同)

Raf*_*ffi 6

我建议改用.values您的Participantqueryset 方法。

简而言之,您想要做的是:

Participant.objects\
    .filter(is_paid=True)\
    .values('event')\
    .distinct()\
    .annotate(models.Count('id'))
Run Code Online (Sandbox Code Playgroud)

完整的示例如下:

  1. 创建2 Event秒:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
    
    Run Code Online (Sandbox Code Playgroud)
  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)]
    
    Run Code Online (Sandbox Code Playgroud)
  3. 将所有Participants按其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)...']>
    
    Run Code Online (Sandbox Code Playgroud)

    这里需要不同的:

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

    什么.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}]>
    
    Run Code Online (Sandbox Code Playgroud)
  5. 最后,您只Participant需要一个is_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}]>
    
    Run Code Online (Sandbox Code Playgroud)

唯一的缺点是Event您只能id从上面的方法中获取,因此您必须检索之后的内容。