【问题标题】:Need a workaround to filter on related model and aggregated fields in Django需要一种解决方法来过滤 Django 中的相关模型和聚合字段
【发布时间】:2010-05-02 23:10:14
【问题描述】:

我为这个问题打开了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 之外,还有其他解决方法吗?

【问题讨论】:

    标签: python sql django orm


    【解决方案1】:

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

    因此,您的结果的有效 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”调用中获取额外的值。我们可以通过滥用“extra”来解决这个问题,尽管这会将表名硬编码为“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。

    【讨论】:

    • 我在GROUP BY 子句中关注"app_plan"."cap"
    猜你喜欢
    • 2020-01-30
    • 2017-10-15
    • 1970-01-01
    • 1970-01-01
    • 2016-07-23
    • 2015-12-23
    • 2018-01-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多