Django中GROUP BY中注释的聚合

Posted

技术标签:

【中文标题】Django中GROUP BY中注释的聚合【英文标题】:Aggregation of an annotation in GROUP BY in Django 【发布时间】:2017-08-17 20:52:22 【问题描述】:

更新

感谢发布的答案,我找到了一种更简单的方法来解决问题。原始问题可以在修订历史中看到。

问题

我正在尝试将 SQL 查询转换为 Django,但收到一个我不理解的错误。

这是我的 Django 模型:

class Title(models.Model):
  title_id = models.CharField(primary_key=True, max_length=12)
  title = models.CharField(max_length=80)
  publisher = models.CharField(max_length=100)
  price = models.DecimalField(decimal_places=2, blank=True, null=True)

我有以下数据:

publisher                    title_id      price  title
---------------------------  ----------  -------  -----------------------------------
New Age Books                PS2106         7     Life Without Fear
New Age Books                PS2091        10.95  Is Anger the Enemy?
New Age Books                BU2075         2.99  You Can Combat    Computer Stress!
New Age Books                TC7777        14.99  Sushi, Anyone?
Binnet & Hardley             MC3021         2.99  The Gourmet Microwave
Binnet & Hardley             MC2222        19.99  Silicon Valley   Gastronomic Treats
Algodata Infosystems         PC1035        22.95  But Is It User Friendly?
Algodata Infosystems         BU1032        19.99  The Busy Executive's   Database Guide
Algodata Infosystems         PC8888        20     Secrets of Silicon Valley

这是我想要做的:引入一个注释字段dbl_price,它是价格的两倍,然后将结果查询集按publisher 分组,并为每个发布者计算所有dbl_price 值的总和该出版商出版的书名。

执行此操作的 SQL 查询如下:

SELECT SUM(dbl_price) AS total_dbl_price, publisher
FROM (
  SELECT price * 2 AS dbl_price, publisher
  FROM title
) AS A 
GROUP BY publisher

期望的输出是:

publisher                    tot_dbl_prices
---------------------------  --------------
Algodata Infosystems                 125.88
Binnet & Hardley                      45.96
New Age Books                         71.86 

Django 查询

查询看起来像:

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(tot_dbl_prices=Sum('dbl_price'))

但报错:

KeyError: 'dbl_price'. 

表示在查询集中找不到字段dbl_price

错误原因

这就是发生此错误的原因:the documentation says

您还应该注意,average_rating 已明确包含在内 在要返回的值列表中。这是必需的,因为 values() 和 annotate() 子句的顺序。

如果 values() 子句在 annotate() 子句之前,任何注释 将自动添加到结果集中。但是,如果 values() 子句在 annotate() 子句之后应用,您需要显式包含聚合列。

因此,dbl_price 无法在聚合中找到,因为它是由先前的 annotate 创建的,但未包含在 values() 中。

但是,我也不能将它包含在values 中,因为我想使用values(后跟另一个annotate)作为分组设备,因为

如果 values() 子句在 annotate() 之前,注释将使用 values() 子句描述的分组计算。

这是 Django implements SQL GROUP BY 的基础。这意味着我不能在values() 中包含dbl_price,因为这样分组将基于publisherdbl_price 这两个字段的唯一组合,而我只需要按publisher 分组。

所以,下面的查询,它与上面的唯一不同之处在于我聚合了模型的 price 字段而不是带注释的 dbl_price 字段,实际上是有效的:

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(sum_of_prices=Count('price'))

因为price 字段在模型中而不是注释字段,因此我们不需要将其包含在values 中以将其保留在查询集中。

问题

所以,我们有了它:我需要在 values 中包含带注释的属性以将其保留在查询集中,但我不能这样做,因为 values 也用于分组(这将是错误的额外字段)。问题本质上是由于 values 在 Django 中使用的两种非常不同的方式,具体取决于上下文(values 是否后跟annotate) - 这是(1)值提取(SQL 普通 @ 987654355@ list) 和 (2) 分组 + 对组的聚合 (SQL GROUP BY) - 在这种情况下,这两种方式似乎有冲突。

我的问题是:有什么办法可以解决这个问题(无需回退到原始 sql)?

请注意:有问题的具体示例可以通过将所有annotate 语句移到values 之后来解决,几个答案都指出了这一点。但是,我对将annotate 语句保留在values() 之前的解决方案(或讨论)更感兴趣,原因有三个: 1. 还有更复杂的示例,其中建议的解决方法不起作用。 2. 我可以想象这样的情况,带注释的查询集已传递给另一个函数,该函数实际上执行 GROUP BY,因此我们唯一知道的是带注释字段的名称集及其类型。 3. 情况似乎很简单,如果values() 的两种不同用途的这种冲突以前没有被注意到和讨论过,我会感到惊讶。

【问题讨论】:

如果你还没有,你可以做一件事,就是在查询集被评估时打印构造的 SQL 字符串,这样你可以尝试切换事情的顺序,直到你得到原始您尝试模拟的 SQL 查询 @Mojimi 感谢您的建议。但是我对通过尝试使上面的特定示例起作用并不真正感兴趣。我有兴趣了解如何使这项工作总体上进行,最好只使用记录在案的用户级 Django 功能,或者这只是无法完成,对于一般查询类,您可以获得一些带注释的属性然后想要聚合在 GROUP BY 中覆盖它。 @LeonidShifrin 根据聊天,我可以得出结论,您发现原始查询是要走的路。如果是这种情况,请在此处发布答案,说明未找到其他替代方案并将其标记为已接受。 @ThulasiRam 我还不确定。在我看来,原始查询是最后的手段,除非我确定这是唯一的答案,否则我不会乐意将其发布为答案。我还没有时间尝试其他事情。一旦我得到满意的答案,我一定会在这里发布,除非有人先发布。 【参考方案1】:

更新:从 Django 2.1 开始,一切都是开箱即用的。不需要变通方法,生成的查询是正确的。

这可能有点太晚了,但我找到了解决方案(用 Django 1.11.1 测试)。

问题是,调用.values('publisher'),这是提供分组所必需的,会删除所有未包含在.values() 字段参数中的注释。

我们不能将dbl_price 包含到fields 参数中,因为它会添加另一个GROUP BY 语句。

制作所有聚合的解决方案,首先需要注释字段,然后调用.values()并将聚合包含到fields参数中(这不会添加GROUP BY,因为它们是聚合)。 然后我们应该使用 ANY 表达式调用.annotate() - 这将使 django 使用查询中唯一的非聚合字段 - publisherGROUP BY 语句添加到 SQL 查询中。

Title.objects
    .annotate(dbl_price=2*F('price'))
    .annotate(sum_of_prices=Sum('dbl_price'))
    .values('publisher', 'sum_of_prices')
    .annotate(titles_count=Count('id'))

这种方法的唯一缺点 - 如果您不需要任何其他聚合,除了带有注释字段的聚合 - 无论如何您都必须包含一些。如果没有最后一次调用 .annotate() (并且它应该至少包含一个表达式!),Django 不会将 GROUP BY 添加到 SQL 查询中。处理此问题的一种方法是创建您的字段的副本:

Title.objects
    .annotate(dbl_price=2*F('price'))
    .annotate(_sum_of_prices=Sum('dbl_price')) # note the underscore!
    .values('publisher', '_sum_of_prices')
    .annotate(sum_of_prices=F('_sum_of_prices')

另外,请注意,您应该小心使用 QuerySet 排序。你最好打电话给.order_by() 或者不带参数来清除排序或者带你GROUP BY 字段。如果结果查询将包含任何其他字段的排序,则分组将是错误的。 https://docs.djangoproject.com/en/1.11/topics/db/aggregation/#interaction-with-default-ordering-or-order-by

此外,您可能希望从输出中删除该假注释,因此请再次调用 .values()。 因此,最终代码如下所示:

Title.objects
    .annotate(dbl_price=2*F('price'))
    .annotate(_sum_of_prices=Sum('dbl_price'))
    .values('publisher', '_sum_of_prices')
    .annotate(sum_of_prices=F('_sum_of_prices'))
    .values('publisher', 'sum_of_prices')
    .order_by('publisher')

【讨论】:

这太棒了!你破解了一个困扰很多django开发者很久的问题。 这可能是了解我在 SO 上看到的一些 ORM 技术的最佳答案之一。 抱歉这么晚才来。我已经看到你的答案很长时间了,但是在问了这个问题之后,我很快就从 Django 转到了 SQLAlchemy。不过,这并不能原谅这么长时间的延迟。非常优雅的解决方案,它揭示了一些内部逻辑并扩展了看似可能的限制。非常感谢。 在我的情况下,在最后一个注释中使用 F() 的提示不起作用,我必须使用真正的聚合来触发正确的分组,例如。 Count().【参考方案2】:

@alexandr 的这个解决方案正确地解决了这个问题。

https://***.com/a/44915227/6323666

你需要的是这样的:

from django.db.models import Sum

Title.objects.values('publisher').annotate(tot_dbl_prices=2*Sum('price'))

理想情况下,我通过先将它们相加然后将其加倍来反转这里的情况。你试图把它加倍然后总结。希望这没问题。

【讨论】:

嗯,我真正感兴趣的问题不是如何执行这个特定的操作。真正的问题是如何分组,在注释字段而不是模型字段上计算聚合。问题中的问题只是说明困难的一个例子。感谢您的回复,您肯定会得到我的 +1,但这并不能真正解决主要困难:当您分组时,您使用 values + annotate。在values 中,您只列出您分组的字段。但是,除非在values 中也列出,否则所有先前注释的属性都会丢失。 所以,当您拥有annotate(prop=...).values('otherprop').annotate(my_aggregate= Sum('prop')) 时,您就不能这样做,因为当您执行第二个annotate 时,prop 已经丢失了。为了不丢失它,您必须将它包含在values 中。但是,您不是在otherprop 上进行分组,而是在otherpropprop 的独特组合上进行分组。问题就在那里,因为values 在 Django 中以两种不同的方式使用,并且在这个特定的设置中,它们确实存在冲突。我想知道这个一般问题是否存在惯用的解决方案,而不是回到原始 sql。 @LeonidShifrin,出于好奇,您能否提供一个示例,说明 Thulasi 建议的方法,即在分组后移动所有注释(即 values 调用)不起作用? @SergGr 我想我可以。但这会稍微复杂一些。想象一下,我们在Title 模型和Author 模型之间有一个事实上的M2M 关系,通过中间的TitleAuthor 模型具有像authortitle 这样的字段,这些字段是这些字段的外键,可能还有一些附加字段。现在,例如,我可能想用作者数量注释每本书,然后按出版商分组,然后计算每个出版商的平均作者数量。 @LeonidShifrin,我不是 Django 专家,但在快速浏览了 model.Querymodel.sql.Query 的代码后,我得出结论,它们根本不是为了生成子查询而设计的除了使用model.sql.Query.get_aggregationmodel.Query.aggregate 调用之外,如果您想要一个比普通SQL 更高级别的解决方法,您可能会发现有趣的灵感。【参考方案3】:

这是 Django 中 group_by works 的预期方式。所有带注释的字段都添加到GROUP BY 子句中。但是,我无法评论为什么会这样写。

你可以让你的查询像这样工作:

Title.objects
  .values('publisher')
  .annotate(total_dbl_price=Sum(2*F('price'))

产生以下 SQL:

SELECT publisher, SUM((2 * price)) AS total_dbl_price
FROM title
GROUP BY publisher

这恰好适用于您的情况。

我知道这可能不是您正在寻找的完整解决方案,但使用CombinedExpressions(我希望!)也可以在此解决方案中容纳一些甚至复杂的注释。

【讨论】:

谢谢!这或多或少是Thulasi Ram 在他的回答中所建议的。这解决了我在问题中的特定示例,但这并不能解决我感兴趣的一般情况(请参阅上述答案下方的 cmets,特别是 @SergGr 和我自己之间的交流)。尽管如此,你的回答增加了我的信心,我需要深入研究 Django 源代码并尝试其他事情,因为看起来一般来说没有简单的方法来解决这种情况,只使用***用户公开Django 功能。 看起来这个问题真的很难解决,主要是当一个人有类似嵌套的 GROUP BY 或其他最初注释的字段实际上是多个字段的聚合(可能是相关模型)的情况。如果仅在原始表的单行上计算带注释的字段,则注释似乎确实与 GROUP BY 通勤,因此您建议的解决方案确实有效。【参考方案4】:

您的问题来自values(),然后是annotate()。顺序很重要。 这在有关[注释和值子句的顺序]的文档中进行了解释( https://docs.djangoproject.com/en/1.10/topics/db/aggregation/#order-of-annotate-and-values-clauses)

.values('pub_id') 使用pub_id 限制查询集字段。所以你不能在income上注释

values() 方法接受可选的位置参数,*fields, 它指定了 SELECT 应该被限制的字段名称。

【讨论】:

感谢您的尝试,但事实并非如此。如果我在title_id 等其他字段上进行注释,则一切正常(如果我将annotate(total_income=Sum('income')) 更改为annotate(total_titles=Count('title_id')) - 并且title_id 也不在values 的字段列表中)。是的,我知道顺序很重要,特别是在 values 之后的 annotate 用于在 Django 中实现 GROUP BY + 聚合——这就是我在这种情况下所需要的。 好的。我的坏:) 问题。 Title_id 是模型的一个字段,income 是一个聚合。这可能是 title_id 在这种情况下可以工作的原因? 实际上,您可能部分正确。文档说 如果 values() 子句在 annotate() 子句之前,任何注释都将自动添加到结果集中。但是,如果在 annotate() 子句之后应用 values() 子句,则需要显式包含聚合列,因此看起来我需要在 values() 中显式列出聚合列。但是,似乎没有办法只按非聚合字段分组。 是的,这肯定是原因。而且我认为您对文档的引用可以解释它。我似乎需要将聚合字段明确包含到values() 中。但问题是,values 也用于 GROUP BY - 然后应该只包含我们分组依据的字段。因此,我需要将聚合字段包含到values() 中以保留它,但我需要将其排除在不按其分组的情况下。这就是问题。它来自于values 以两种非常不同的方式使用这一事实,具体取决于annotate 是否跟随。在这种情况下,这些方式似乎相互冲突 非常感谢您花时间回答。我认为无论如何这都为我指明了正确的方向。非常感谢。

以上是关于Django中GROUP BY中注释的聚合的主要内容,如果未能解决你的问题,请参考以下文章

Django GROUP BY strftime 日期格式

带有注释的Django查询集,为啥将GROUP BY应用于所有字段?

Django ORM 使用过滤器进行注释

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

在某些情况下,注释会导致 OperationalError “字段不在 GROUP BY 中”

Django 与 Postgresql,列必须出现在 GROUP BY 子句中或在聚合函数中使用