如何在 Django 查询集中使用条件注释 Count

Posted

技术标签:

【中文标题】如何在 Django 查询集中使用条件注释 Count【英文标题】:How to annotate Count with a condition in a Django queryset 【发布时间】:2016-02-19 21:22:44 【问题描述】:

使用 Django ORM,可以做类似queryset.objects.annotate(Count('queryset_objects', gte=VALUE)) 的事情。赶上我的漂移?


这里有一个简单的例子来说明一个可能的答案:

在 Django 网站中,内容创建者提交文章,普通用户查看(即阅读)所述文章。文章既可以发表(即可供所有人阅读),也可以以草稿模式发表。描述这些要求的模型是:

class Article(models.Model):
    author = models.ForeignKey(User)
    published = models.BooleanField(default=False)

class Readership(models.Model):
    reader = models.ForeignKey(User)
    which_article = models.ForeignKey(Article)
    what_time = models.DateTimeField(auto_now_add=True)

我的问题是:我怎样才能获得所有已发表的文章,按过去 30 分钟内的独特读者群排序? IE。我想计算每篇已发表文章在过去半小时内获得了多少不同(唯一)浏览量,然后生成按这些不同浏览量排序的文章列表。


我试过了:

date = datetime.now()-timedelta(minutes=30)
articles = Article.objects.filter(published=True).extra(select = 
  "views" : """
  SELECT COUNT(*)
  FROM myapp_readership
    JOIN myapp_article on myapp_readership.which_article_id = myapp_article.id
  WHERE myapp_readership.reader_id = myapp_user.id
  AND myapp_readership.what_time > %s """ % date,
).order_by("-views")

这引发了错误:“01”或附近的语法错误(其中“01”是额外内部的日期时间对象)。没什么可继续的。

【问题讨论】:

with unique view 是否意味着同一读者的多次浏览都算作一次? 没错,多次浏览,但同一个人的浏览,算作一次。我认为这里需要使用 distinct()。 看看这是否为您提供了每篇文章的唯一查看次数,然后您将过滤/排序/限制它们Article.objects.values('readership__reader').annotate(numviews=Count('readership')) 是的,我也做到了这一点,但是当我尝试让“在最后 30 分钟内”工作时,一切都消失了。 尝试Article.objects.values('readership__reader').annotate(numviews=Count('readership‌​')).filter(readership__what_time__lt=threshold),其中threshold = datetime.datetime.now() - timedelta(minutes=30) 【参考方案1】:

对于 django >= 1.8

使用Conditional Aggregation:

from django.db.models import Count, Case, When, IntegerField
Article.objects.annotate(
    numviews=Count(Case(
        When(readership__what_time__lt=treshold, then=1),
        output_field=IntegerField(),
    ))
)

解释: 通过您的文章进行的正常查询将使用numviews 字段进行注释。该字段将构造为由 Count 包装的 CASE/WHEN 表达式,它将返回 1 表示读者群匹配条件,NULL 表示读者群不匹配条件。 Count 将忽略空值并仅计算值。

您将在最近未查看的文章中获得零分,您可以使用 numviews 字段进行排序和过滤。

PostgreSQL 后面的查询将是:

SELECT
    "app_article"."id",
    "app_article"."author",
    "app_article"."published",
    COUNT(
        CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN 1
        ELSE NULL END
    ) as "numviews"
FROM "app_article" LEFT OUTER JOIN "app_readership"
    ON ("app_article"."id" = "app_readership"."which_article_id")
GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"

如果我们只想跟踪唯一查询,我们可以在Count 中添加区分,并使我们的When 子句返回值,我们想要区分。

from django.db.models import Count, Case, When, CharField, F
Article.objects.annotate(
    numviews=Count(Case(
        When(readership__what_time__lt=treshold, then=F('readership__reader')), # it can be also `readership__reader_id`, it doesn't matter
        output_field=CharField(),
    ), distinct=True)
)

这将产生:

SELECT
    "app_article"."id",
    "app_article"."author",
    "app_article"."published",
    COUNT(
        DISTINCT CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN "app_readership"."reader_id"
        ELSE NULL END
    ) as "numviews"
FROM "app_article" LEFT OUTER JOIN "app_readership"
    ON ("app_article"."id" = "app_readership"."which_article_id")
GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"

对于 django

您可以只使用raw 来执行由较新版本的 django 创建的 SQL 语句。显然,不使用raw 就没有简单且优化的方法来查询该数据(即使使用extra,注入必需的JOIN 子句也存在一些问题)。

Articles.objects.raw('SELECT'
    '    "app_article"."id",'
    '    "app_article"."author",'
    '    "app_article"."published",'
    '    COUNT('
    '        DISTINCT CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN "app_readership"."reader_id"'
    '        ELSE NULL END'
    '    ) as "numviews"'
    'FROM "app_article" LEFT OUTER JOIN "app_readership"'
    '    ON ("app_article"."id" = "app_readership"."which_article_id")'
    'GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"')

【讨论】:

优秀的解决方案,点赞。不过,它是否计算独特的观看次数? @Hassan Baig 不幸的是,对于较旧的 django 版本,您只能通过 extra 或原始查询来完成。 是的,我就是这么想的。例如,这是我尝试的一个额外查询(给了我一个语法错误):articles = Article.objects.filter(published=True).extra(select='views':'SELECT COUNT (DISTINCT reader_id) FROM myapp_readership AS readership WHERE (readership.what_time &gt; %s AND readership.which_article_id=readership.which_article_id)',select_params=(‌​threshold,),).order_by('-views') threshold 是 30 分钟的时间增量。 @Pynchia 当然。我们可以将distinct=True 添加到Count 并返回我们想要区分的值,而不是1。 @HassanBaig 在这种情况下放置子查询会非常慢,但是您可以使用 django >= 1.8 在您extra 中创建的查询中的 SQL 代码,只需要欺骗 django,所以它会为我们创建连接和分组。请参阅我的更新答案。【参考方案2】:

对于 django >= 2.0,您可以在聚合函数中使用 Conditional aggregation with a filter argument:

from datetime import timedelta
from django.utils import timezone
from django.db.models import Count, Q # need import

Article.objects.annotate(
    numviews=Count(
        'readership__reader__id', 
        filter=Q(readership__what_time__gt=timezone.now() - timedelta(minutes=30)), 
        distinct=True
    )
)

【讨论】:

以上是关于如何在 Django 查询集中使用条件注释 Count的主要内容,如果未能解决你的问题,请参考以下文章

Django 多注解返回错误结果

Django annotate() 多次导致错误答案

Django:从查询集中删除过滤条件

根据条件使用 ForeignKey Counts 注释 Django 查询集

Django 使用注释更新查询集

如何使用 StringAgg 或 ArrayAgg 连接多个子行中的一列来注释 django 查询集?