如何在 Django 中过滤对象以进行计数注释?

Posted

技术标签:

【中文标题】如何在 Django 中过滤对象以进行计数注释?【英文标题】:How to filter objects for count annotation in Django? 【发布时间】:2015-08-25 11:09:35 【问题描述】:

考虑简单的 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 注释值。

example from documentation 在这里不起作用,因为它从查询中排除对象,而不是用 0 注释它们。

更新。 Django 1.8 有了新的conditional expressions feature,所以现在我们可以这样做:

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 具有新的Conditional aggregation 功能,请参阅下面的the accepted answer。

更新 3。 对于 Django 3.x,请check this answer below。

【问题讨论】:

【参考方案1】:

Django 2.0 中的Conditional aggregation 允许您进一步减少过去的faff 数量。这也将使用 Postgres 的 filter 逻辑,这比 sum-case 快一些(我见过像 20-30% 这样的数字)。

无论如何,在你的情况下,我们正在研究这样简单的事情:

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

关于filtering on annotations 的文档中有一个单独的部分。它与条件聚合相同,但更像我上面的示例。无论哪种方式,这都比我之前做的那些粗糙的子查询要健康得多。

【讨论】:

顺便说一句,文档链接中没有这样的示例,仅显示了 aggregate 的用法。您是否已经测试过此类查询? (我没有,我想相信!:) 我有。他们工作。实际上,我遇到了一个奇怪的补丁,其中一个旧的(超级复杂的)子查询在升级到 Django 2.0 后停止工作,我设法用一个超级简单的过滤计数替换它。有一个更好的文档内注释示例,所以我现在将其拉入。 这里有几个答案,这是django 2.0的方式,下面你会找到django 1.11(子查询)的方式,还有django 1.8的方式。 当心,如果你在 Django 将毫无例外地运行,但过滤器根本没有应用。所以它可能看起来适用于 Django 如果您需要添加多个过滤器,您可以将它们添加到 Q() 参数中,用 分隔,例如 filter=Q(participants__is_paid=True, somethingelse=value)【参考方案2】:

刚刚发现 Django 1.8 有新的conditional expressions feature,所以现在我们可以这样做:

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 提出的问题是这对于大型集合是否效率低下,而不是它是否会起作用......对吗?最重要的是要知道它不是在 python 中做的,它是创建一个 SQL case 子句 - 参见github.com/django/django/blob/master/django/db/models/… - 所以它的性能相当不错,简单的例子比连接更好,但更复杂的版本可能包括子查询等 当与Count(而不是Sum)一起使用时,我想我们应该设置default=None(如果不使用django 2 filter 参数)。【参考方案3】:

更新

我提到的子查询方法现在通过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`"
)

【讨论】:

谢谢托多!似乎我已经找到了不使用.extra 的方法,因为我更喜欢在 Django 中避免使用 SQL :) 我会更新这个问题。 不客气,顺便说一句,我知道这种方法,但直到现在它还是一个不起作用的解决方案,这就是我没有提到它的原因。但是我刚刚发现它已在Django 1.8.2 中修复,所以我猜你使用的是那个版本,这就是它为你工作的原因。你可以阅读更多关于here和here 我知道当它应该为 0 时会产生 None。还有其他人得到这个吗? @StefanJCollier 是的,我也收到了None。我的解决方案是使用Coalesce (from django.db.models.functions import Coalesce)。你可以这样使用它:Coalesce(Subquery(...), 0)。不过,可能有更好的方法。 这很棒,因为下面 Oli 更赞成的答案中的方法在可读性方面“更好”,导致 mysql 上的“LEFT OUTER JOIN”。这在性能方面非常不友好。所以赞成这两个答案!【参考方案4】:

我建议改用 Participant 查询集的 .values 方法。

简而言之,你想做的事情是:

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

一个完整的例子如下:

    创建2个Events:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
    

    给他们添加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)]
    

    event 字段对所有Participants 进行分组:

    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 在这里所做的是,他们正在创建两个按元素 event 分组的 Participants 桶。请注意,这些存储桶包含 Participant

    然后您可以注释这些存储桶,因为它们包含原始Participant 的集合。这里我们要计算Participant的数量,这只是通过计算那些桶中元素的ids来完成的(因为那些是Participant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet ['event': 1, 'id__count': 10, 'event': 2, 'id__count': 50]>
    

    最后你只想要Participantis_paidTrue,你可以在前面的表达式前面添加一个过滤器,这会产生如上所示的表达式:

    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

【讨论】:

aggregation docs 还讨论了values()annotate() 一起使用。【参考方案5】:

我在寻找什么结果:

已将任务添加到报表的人员(受让人)。 - 总唯一 人数 已将任务添加到报告中但对于任务 可计费性仅大于 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]>

【讨论】:

【参考方案6】:

对于 Django 3.x,只需在注释后编写过滤器:

User.objects.values('user_id')
            .annotate(xyz=models.Sum('likes'))
            .filter(xyz__gt=100)

在上面的 xyz 不是 User Model 中的模型字段,这里我们过滤了喜欢(或 xyz)超过 100 的用户。

【讨论】:

以上是关于如何在 Django 中过滤对象以进行计数注释?的主要内容,如果未能解决你的问题,请参考以下文章

Django:结合来自两个过滤器查询的两个计数注释

在 Django 中注释相关和多重过滤的对象

Django ORM:如何根据注释 timedelta 结果进行过滤

如何在 django 注释中从 0 开始计数?

如何过滤和统计 DJANGO 模板中的对象?

Django 在模板标签中过滤和计数