如何在Django中过滤用于计数注释的对象?


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

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

Answers:


105

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

在文档中有一个单独的部分,关于对注释进行过滤。它和条件聚合是一样的东西,但是更像上面的例子。无论哪种方式,这都比我以前做的粗糙子查询要健康得多。


顺便说一句,文档链接中没有这样的示例,仅显示aggregate用法。您是否已经测试过此类查询?(我没有,我想相信!:)
rudyryk

2
我有。他们工作。我实际上遇到了一个怪异的补丁,其中一个旧的(超级复杂的)子查询在升级到Django 2.0后停止工作,我设法用一个超简单的过滤计数替换了它。有一个更好的文档内注释示例,所以我现在将其引入。
奥利

1
这里有一些答案,这是Django 2.0方式,下面您将找到Django 1.11(子查询)方式和Django 1.8方式。
Ryan Castner '18

2
当心,如果你尝试这种在Django <2,例如1.9,它无一例外运行,但过滤器根本不适用。因此,它似乎可以与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 '17

为什么不?我的意思是,为什么您的情况不同?在上述情况下,活动中可以有任意数量的付费参与者。
rudyryk

我认为@SverkerSbrg提出的问题是,这对于大型集合而言是否效率低下,而不是它是否行得通..对吗?要知道的最重要的事情是,它不是在python中执行的,而是在创建SQL case子句-请参见github.com/django/django/blob/master/django/db/models/…-这样它的性能就可以了,简单的例子会更好,比加入,但更复杂的版本可能包括的子查询等
海登克罗克

1
当与Count(而不是Sum)一起使用时,我想我们应该进行设置default=None(如果不使用django 2 filter参数)。
djvg

41

更新

Django 1.11现在通过subquery-expressions支持了我提到的子查询方法。

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`"
})

谢谢多多!似乎我已经找到了不使用的方法.extra,因为我更喜欢避免在Django中使用SQL :)我将更新问题。
rudyryk

1
不用客气,顺便说一句,我知道这种方法,但是直到现在,它仍然是一个无效的解决方案,这就是为什么我没有提到它。但是,我刚刚发现它已在中修复Django 1.8.2,因此我想您使用的是该版本,这就是它为您工作的原因。你可以阅读更多有关在这里这里
托多尔

2
我得到的是,当它应该为0时会产生None。
StefanJCollier

@StefanJCollier是的,我None也知道。我的解决方案是使用Coalescefrom django.db.models.functions import Coalesce)。您可以这样使用:Coalesce(Subquery(...), 0)。不过,可能有更好的方法。
亚当·泰勒

6

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

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

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

    这里需要与众不同:

    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_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的任务的人员。

通常,我将不得不使用两个不同的查询:

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}]>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.