Django ORM group by,并找到每个组的最新项目(窗口函数)

Posted

技术标签:

【中文标题】Django ORM group by,并找到每个组的最新项目(窗口函数)【英文标题】:Django ORM group by, and find latest item of each group (window functions) 【发布时间】:2018-08-28 06:12:51 【问题描述】:

假设我们有一个模型如下

class Cake(models.Model):

    baked_on = models.DateTimeField(auto_now_add=True)
    cake_name = models.CharField(max_length=20)

现在,有多个Cakes 在同一天烘烤,我需要一个查询,它会返回一个月度蛋糕报告,其中包含当月的每一天,以及第一个和最后一个烘烤的蛋糕的名称那天。

例如,如果数据是这样的:

baked_on        cake_name
11 Jan 12:30    Vanilla
11 Jan 14:30    Strawberry
11 Jan 20:45    Avocado
12 Jan 09:05    Raspberry
12 Jan 16:30    Sprinkles
12 Jan 20:11    Chocolate

我的查询输出应该是这样的

date    first     last
11 Jan  Vanilla   Avocado
12 Jan  Raspberry Chocolate

我应该如何在单个 ORM 调用中执行此操作?

【问题讨论】:

【参考方案1】:

Django 2.0 引入了针对此类查询的窗口函数。您的问题的简单答案是:

Cake.objects.annotate(
    first_cake=Window(
        expression=FirstValue('cake_name'),
        partition_by=[TruncDate('baked_on')],
        order_by=F('baked_on').asc(),
    ),
    last_cake=Window(
        expression=FirstValue('cake_name'),
        partition_by=[TruncDate('baked_on')],
        order_by=F('baked_on').desc(),
    ),
    day=TruncDate('baked_on'),
).distinct().values_list('day', 'first_cake', 'last_cake')

为什么在last_cake 中有FirstValue?这是因为默认情况下窗口查询将遍历每一行并且不会向前看,因此对于每一行,最后一行将等于当前行。使用last_row 和降序排序可以解决这个问题。或者你可以定义窗口查询应该工作的框架:

Cake.objects.annotate(
    first_cake=Window(
        expression=FirstValue('cake_name'),
        partition_by=[TruncDate('baked_on')],
        order_by=F('baked_on').asc(),
    ),
    last_cake=Window(
        expression=LastValue('cake_name'),
        partition_by=[TruncDate('baked_on')],
        order_by=F('baked_on').asc(),
        frame=ValueRange(),
    ),
    day=TruncDate('baked_on'),
).distinct().values_list('day', 'first_cake', 'last_cake')

【讨论】:

看起来是一个非常干净的解决方案,尽管我乍一看并不完全理解它!只是为了确认一下,Django 1.11 不支持这个是吗? 不,窗口表达式是在 2.0 中引入的,在 1.11 中不可用。 你怎么能在 TruncDate 之外进行自定义,就像我想使用 TruncMinute,但得到 5 分钟的块而不是 1 分钟的块?我使用您的解决方案完成了这项工作,并使用了 1 分钟的块。 可以尝试指定partition_by如下:[TruncHour('baked_on'), Cast(ExtractMinute('baked_on') / 5, IntegerField())] @GwynBleidD 任何想法,当我这样做 partition_by Cast(ExtractMinute('baked_on') / 15, IntegerField()) 时,我有时会以每小时超过 4 次结束。平均而言,我每小时得到 5 个而不是 4 个。我应该少 3 倍,平均而言我只少 2.4 倍。

以上是关于Django ORM group by,并找到每个组的最新项目(窗口函数)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 django orm 进行嵌套 Group By?

Django ORM,group_by 按所有值分组

django模型orm进行group by

使用SQL语言了解Django ORM中的分组(group by)和聚合(aggregation)查询

如何在 django orm 中使用 Annotation 进行嵌套 Group By?

django的group_by