【问题标题】:How to filter objects for count annotation in Django?如何在 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

【问题讨论】:

    标签: python django django-models django-aggregation


    【解决方案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 中修复,所以我猜你使用的是那个版本,这就是它为你工作的原因。你可以阅读更多关于herehere
    • 我知道当它应该为 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'))
    

    一个完整的例子如下:

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

    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. 最后你只想要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

    【讨论】:

    【解决方案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 的用户。

      【讨论】:

        猜你喜欢
        • 2011-03-31
        • 2012-03-07
        • 2019-12-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-25
        • 2021-06-10
        • 1970-01-01
        相关资源
        最近更新 更多