【问题标题】:Complex GROUP BY with Django's ORM使用 Django 的 ORM 进行复杂的 GROUP BY
【发布时间】:2016-04-17 08:31:51
【问题描述】:

我有一个跟踪电力消耗的 Django 应用程序,我很难想出一种方法来使用 Django 的 ORM 来获取一些信息。

我的具体用例是这样的:我有一组用电量读数,每个读数都有一个日期时间字段、消耗量和成本(还有一些其他的,但这些是相关的)。我需要对按月、年、电表和电价分组的消耗和成本值求和。换句话说,我需要能够获得每个月、每年、每个价格的总能耗值和相应的成本(如果你看一下后面的表格更容易理解)。

这是我的 ElectricityReading 模型及其父模型 Reading (分开是因为我们还有水和天然气的消耗读数,这也来自 Reading):

from model_utils.models import TimeStampedModel
# Other imports here...

class Reading(TimeStampedModel):
    meter = models.ForeignKey(Meter)
    datetime = models.DateTimeField() # Terrible property name, I know :)

    class Meta:
        abstract = True

class ElectricityReading(Reading):
    price = models.ForeignKey(ElectricityPrice)
    consumption = models.DecimalField(max_digits=18, decimal_places=3,
                                  null=True, blank=True, default=None)
    cost = models.DecimalField(max_digits=18, decimal_places=3, null=True,
                           blank=True, default=None)

现在我正在使用这个原始 SQL 执行此操作,我根据几个参数构建它:

SELECT
    (EXTRACT(YEAR FROM datetime)) AS reading_date_year,
    (EXTRACT(MONTH FROM datetime)) AS reading_date_month,
    SUM(consumption) as total_consumption,
    SUM(cost) as total_cost,
    COUNT(id) as num_readings,
    price_id
FROM electricity_reading
WHERE meter_id IN (10)
    AND datetime >= '2015-10-01 00:00'
    AND datetime <= '2015-12-31 23:59'
GROUP BY reading_date_year, reading_date_month, price_id, meter_id
ORDER BY meter_id, reading_date_year, reading_date_month, price_id

此 SQL 查询产生类似于以下数据的结果(组成值和简化的列名以便更好地格式化):

╔══════╦═══════╦═════════════╦══════╦═════════════ ═╦═══════╗ ║年║月║消费║成本║num_readings║价格║ ╠══════╬═══════╬═════════════╬══════╬═════════════ ═╬═══════╣ ║ 2015 ║ 10 ║ 600 ║ 804 ║ 456 ║ 1 ║ ║ 2015 ║ 10 ║ 728 ║ 471 ║ 1998 ║ 2 ║ ║ 2015 ║ 10 ║ 848 ║ 792 ║ 1266 ║ 3 ║ ║ 2015 ║ 10 ║ 256 ║ 705 ║ 744 ║ 5 ║ ║ 2015 ║ 11 ║ 528 ║ 377 ║ 630 ║ 1 ║ ║ 2015 ║ 11 ║ 016 ║ 687 ║ 1680 ║ 2 ║ ║ 2015 ║ 11 ║ 240 ║ 826 ║ 1289 ║ 3 ║ ║ 2015 ║ 11 ║ 736 ║ 522 ║ 720 ║ 5 ║ ║ 2015 ║ 12 ║ 584 ║ 627 ║ 608 ║ 1 ║ ║ 2015 ║ 12 ║ 776 ║ 078 ║ 1627 ║ 2 ║ ║ 2015 ║ 12 ║ 600 ║ 401 ║ 1410 ║ 3 ║ ║ 2015 ║ 12 ║ 864 ║ 842 ║ 744 ║ 5 ║ ╚══════╩═══════╩═════════════╩══════╩═════════════ ═╩═══════╝

使用 Django 的 ORM,我认为我需要的代码大致如下:

objs = ElectricityReading.objects\
    .filter(
        meter=10,
        datetime__gte='2015-05-01 00:00',
        datetime__lte='2015-08-31 23:59'
    ).only('price_id')\
    .annotate(reading_date_year=YearTransform('datetime'))\
    .annotate(reading_date_month=MonthTransform('datetime'))\
    .annotate(total_consumption=Sum('consumption'))\
    .annotate(total_cost=Sum('cost'))\
    .annotate(num_readings=Count('id'))\
    .order_by('meter_id', 'reading_date_year', 'reading_date_month', 'price_id')

但是它生成的SQL不是我需要的:

SELECT
    id,
    price_id,
    EXTRACT('year' FROM datetime AT TIME ZONE 'Europe/Lisbon') AS reading_date_year,
    EXTRACT('month' FROM datetime AT TIME ZONE 'Europe/Lisbon') AS reading_date_month,
    SUM(consumption) AS total_consumption, SUM(cost) AS total_cost,
    COUNT(id) AS num_readings
FROM geratriz_electricityreading
WHERE (
    datetime >= '2015-05-01 00:00:00+01:00'
    AND datetime <= '2015-08-31 23:59:00+01:00'
    AND meter_id = 10)
GROUP BY
    id,
    EXTRACT('year' FROM datetime AT TIME ZONE 'Europe/Lisbon'),
    EXTRACT('month' FROM datetime AT TIME ZONE 'Europe/Lisbon')
ORDER BY meter_id ASC, reading_date_year ASC, reading_date_month ASC, price_id ASC

这会导致从数据库返回更多行,因为没有按照我的需要进行分组。

我似乎无法使用 Django 的 ORM 复制的 SQL 查询部分是末尾的 GROUP BY 子句。 Django坚持按ID分组,我似乎找不到办法让它按meter_id和price_id分组。

考虑到我已经在这方面花费了多少时间,我倾向于说我想要完成的事情根本不可能使用 Django 的 ORM,但我希望有人会告诉我我遗漏了一些东西。

【问题讨论】:

    标签: sql django postgresql django-models django-orm


    【解决方案1】:

    尝试使用values()

    objs = ElectricityReading.objects\
        .filter(
            meter=10,
            datetime__gte='2015-05-01 00:00',
            datetime__lte='2015-08-31 23:59'
        .values('price_id')\
        .annotate(reading_date_year=YearTransform('datetime'))\
        .annotate(reading_date_month=MonthTransform('datetime'))\
        .annotate(total_consumption=Sum('consumption'))\
        .annotate(total_cost=Sum('cost'))\
        .annotate(num_readings=Count('id'))\
        .order_by('meter_id', 'reading_date_year', 'reading_date_month', 'price_id')
    

    这应该将结果分组到price_id。如果您一次显示多个米而不是 meter=10,那么您可以使用 values('price_id', 'meter'),它会在两个字段上分组。

    【讨论】:

    • 似乎完美运行!太感谢了!你无法想象我是多么绝望——而且这是一个如此简单的解决方案,我怎么能在多次阅读 Django 的 ORM 文档后错过它?!我欠你一杯!你有小费罐子、Patreon 页面或任何其他方式可以表达我的感激之情吗?
    • 很高兴它对你有用!我没有小费罐子,只是先帮个忙,然后帮助另一个 Django 用户 :-)
    • 一定会的。再次感谢,阿拉斯代尔! :-)
    猜你喜欢
    • 2018-06-13
    • 2021-11-25
    • 2021-08-01
    • 2019-02-28
    • 2021-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-17
    相关资源
    最近更新 更多