Django中GROUP BY中注释的聚合

Leo*_*rin 26 sql django django-models django-orm

UPDATE

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

问题

我正在尝试将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)
Run Code Online (Sandbox Code Playgroud)

我有以下数据:

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
Run Code Online (Sandbox Code Playgroud)

以下是我想要做的事情:引入一个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
Run Code Online (Sandbox Code Playgroud)

期望的输出是:

publisher                    tot_dbl_prices
---------------------------  --------------
Algodata Infosystems                 125.88
Binnet & Hardley                      45.96
New Age Books                         71.86 
Run Code Online (Sandbox Code Playgroud)

Django查询

查询看起来像:

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(tot_dbl_prices=Sum('dbl_price'))
Run Code Online (Sandbox Code Playgroud)

但是给出了一个错误:

KeyError: 'dbl_price'. 
Run Code Online (Sandbox Code Playgroud)

表示它无法在查询dbl_price集中找到该字段.

出错的原因

这就是为什么会发生这种错误:文档说

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

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

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

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

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

这是Django如何实现SQLGROUP BY的基础.这意味着我可以不包括dbl_pricevalues(),因为这时的分组将根据这两个领域的独特组合publisherdbl_price,而我需要通过组publisher只.

因此,以下查询仅与上面的不同之处在于我聚合模型的price字段而不是注释dbl_price字段,实际上有效:

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(sum_of_prices=Count('price'))
Run Code Online (Sandbox Code Playgroud)

因为该price字段位于模型中而不是带注释的字段,因此我们不需要将其包含在内values以将其保留在查询集中.

这个问题

所以,在这里我们拥有它:我需要包含注释属性values以将其保留在查询集中,但我不能这样做,因为values它也用于分组(这将是一个额外的字段错误).问题主要是由于valuesDjango中使用的两种非常不同的方式,取决于上下文(是否values遵循annotate) - 这是(1)值提取(SQL普通SELECT列表)和(2)分组+聚合组(SQL GROUP BY) - 在这种情况下,这两种方式似乎发生冲突.

我的问题是:有没有办法解决这个问题(没有回到原始sql之类的东西)?

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

Ale*_*nov 21

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

问题是,调用.values('publisher'),提供分组所需的,删除所有注释,这些注释不包括在.values() 字段 param中.

我们不能包含dbl_price字段 param,因为它将添加另一个GROUP BY语句.

要进行所有聚合的解决方案,首先需要带注释的字段,然后调用.values()并将该聚合包含到字段 param(这不会添加GROUP BY,因为它们是聚合).然后我们应该.annotate()使用ANY表达式调用- 这将GROUP BY使用query- publisher中唯一的非聚合字段使django add 语句添加到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'))
Run Code Online (Sandbox Code Playgroud)

使用这种方法的唯一减号 - 如果你不需要任何其他聚合,除了那个带有注释字段的聚合 - 你还是必须包括一些.没有最后调用.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')
Run Code Online (Sandbox Code Playgroud)

另外,请注意,您应该小心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')
Run Code Online (Sandbox Code Playgroud)

  • 这太神奇了!您解决了一个困扰了很多django开发人员很长时间的问题。 (6认同)
  • 这可能是理解我在SO上看到的一些ORM技术的最佳答案之一。 (2认同)

lum*_*s42 5

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

您可以让查询像这样工作:

Title.objects
  .values('publisher')
  .annotate(total_dbl_price=Sum(2*F('price'))
Run Code Online (Sandbox Code Playgroud)

它产生以下 SQL:

SELECT publisher, SUM((2 * price)) AS total_dbl_price
FROM title
GROUP BY publisher
Run Code Online (Sandbox Code Playgroud)

这恰好适用于你的情况。

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