Django 中 GROUP BY 注释的聚合

Aggregation of an annotation in GROUP BY in Django

更新

感谢发布的答案,我找到了一种更简单的方法来表述问题。原题可见修改历史

问题

我正在尝试将 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

You should also note that average_rating has been explicitly included in the list of values to be returned. This is required because of the ordering of the values() and annotate() clause.

If the values() clause precedes the annotate() clause, any annotations will be automatically added to the result set. However, if the values() clause is applied after the annotate() clause, you need to explicitly include the aggregate column.

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

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

If the values() clause precedes the annotate(), the annotation will be computed using the grouping described by the values() clause.

这是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 也是用于分组(额外字段会出错)。问题本质上是由于在 Django 中使用 values 的两种非常不同的方式,具体取决于上下文(values 是否后跟 annotate) - 这是 (1)值提取(SQL 普通 SELECT 列表)和 (2) 分组 + 分组聚合 (SQL GROUP BY) - 在这种情况下,这两种方式似乎冲突。

我的问题是:有没有什么办法可以解决这个问题(不用回退到原始 sql 之类的东西)?

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

您的问题来自 values(),随后是 annotate()。顺序很重要。 这在关于 [order of annotate and values clauses]( https://docs.djangoproject.com/en/1.10/topics/db/aggregation/#order-of-annotate-and-values-clauses)

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

上注释

The values() method takes optional positional arguments, *fields, which specify field names to which the SELECT should be limited.

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

您需要的是:

from django.db.models import Sum

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

理想情况下,我通过先对它们求和然后将其加倍来扭转这里的情况。你试图把它加倍然后总结。希望一切顺利。

这是 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(我希望!),一些甚至复杂的注释也可以包含在该解决方案中。

更新:从 Django 2.1 开始,一切都开箱即用。无需解决方法,生成的查询是正确的。

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

问题是,调用 .values('publisher') 需要提供分组,删除所有未包含在 .values() fields 参数中的注释.

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

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

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')