需要一种解决方法来过滤Django中的相关模型和聚合字段

时间:2010-05-02 23:10:15

标签: python sql django orm

我为这个问题打开了ticket

简而言之,这是我的模特:

class Plan(models.Model):
 cap = models.IntegerField()

class Phone(models.Model):
 plan = models.ForeignKey(Plan, related_name='phones')

class Call(models.Model):
 phone = models.ForeignKey(Phone, related_name='calls')
 cost = models.IntegerField()

我想运行像这样的查询:

Phone.objects.annotate(total_cost=Sum('calls__cost')).filter(total_cost__gte=0.5*F('plan__cap'))

不幸的是,Django会生成错误的SQL:

SELECT "app_phone"."id", "app_phone"."plan_id",
SUM("app_call"."cost") AS "total_cost"
FROM "app_phone"
INNER JOIN "app_plan" ON ("app_phone"."plan_id" = "app_plan"."id")
LEFT OUTER JOIN "app_call" ON ("app_phone"."id" = "app_call"."phone_id")
GROUP BY "app_phone"."id", "app_phone"."plan_id"
HAVING SUM("app_call"."cost") >=  0.5 * "app_plan"."cap"

和错误:

ProgrammingError: column "app_plan.cap" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...."plan_id" HAVING SUM("app_call"."cost") >=  0.5 * "app_plan"....

除了运行原始SQL之外,还有其他解决方法吗?

1 个答案:

答案 0 :(得分:1)

聚合时,SQL要求字段中的任何值在组内是唯一的,或者字段包含在聚合函数中,以确保每个组只有一个值。这里的问题是“app_plan.cap”对于“app_phone.id”和“app_phone.plan_id”的每个组合可能有许多不同的值,因此您需要告诉DB如何处理这些值。

因此,结果的有效SQL是两种不同的可能性之一,具体取决于您想要的结果。首先,您可以在GROUP BY函数中包含app_plan.cap,以便(app_phone.id,app_phone.plan_id,app_plan.cap)的任何不同组合都是不同的组:

SELECT "app_phone"."id", "app_phone"."plan_id", "app_plan"."cap",
SUM("app_call"."cost") AS "total_cost"
FROM "app_phone"
INNER JOIN "app_plan" ON ("app_phone"."plan_id" = "app_plan"."id")
LEFT OUTER JOIN "app_call" ON ("app_phone"."id" = "app_call"."phone_id")
GROUP BY "app_phone"."id", "app_phone"."plan_id", "app_plan"."cap"
HAVING SUM("app_call"."cost") >=  0.5 * "app_plan"."cap"

诀窍是将额外的值放入“GROUP BY”调用中。我们可以通过滥用“额外”来狡猾地处理这个问题,尽管这会对“app_plan”的表名进行硬编码,这是非常不合适的 - 如果您愿意,可以使用Plan类以编程方式进行编写:

Phone.objects.extra({
    "plan_cap": "app_plan.cap"
}).annotate(
    total_cost=Sum('calls__cost')
).filter(total_cost__gte=0.5*F('plan__cap'))

或者,您可以将app_plan.cap包装在聚合函数中,将其转换为唯一值。聚合函数因数据库提供程序而异,但可能包括AVG,MAX,MIN等内容。

SELECT "app_phone"."id", "app_phone"."plan_id",
SUM("app_call"."cost") AS "total_cost",
AVG("app_plan"."cap") AS "avg_cap",
FROM "app_phone"
INNER JOIN "app_plan" ON ("app_phone"."plan_id" = "app_plan"."id")
LEFT OUTER JOIN "app_call" ON ("app_phone"."id" = "app_call"."phone_id")
GROUP BY "app_phone"."id", "app_phone"."plan_id"
HAVING SUM("app_call"."cost") >=  0.5 * AVG("app_plan"."cap")

您可以使用以下方法在Django中获得此结果:

Phone.objects.annotate(
    total_cost=Sum('calls__cost'), 
    avg_cap=Avg('plan__cap')
).filter(total_cost__gte=0.5 * F("avg_cap"))

您可能需要考虑更新您所留下的错误报告,更清楚地说明您期望的结果 - 例如,您所追求的有效SQL。