如何在 Django 中过滤对象以进行计数注释?
Posted
技术标签:
【中文标题】如何在 Django 中过滤对象以进行计数注释?【英文标题】:How to filter objects for count annotation in Django? 【发布时间】:2015-08-25 11:09:35 【问题描述】:考虑简单的 Django 模型 Event
和 Participant
:
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个Event
s:
event1 = Event.objects.create(title='event1')
event2 = Event.objects.create(title='event2')
给他们添加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)]
按event
字段对所有Participant
s 进行分组:
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
分组的 Participant
s 桶。请注意,这些存储桶包含 Participant
。
然后您可以注释这些存储桶,因为它们包含原始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]>
最后你只想要Participant
和is_paid
是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
。
【讨论】:
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 中过滤对象以进行计数注释?的主要内容,如果未能解决你的问题,请参考以下文章