注释自定义 SQL 函数(类似于 date_trunc)到 Django ORM 查询集

Annotate Custom SQL Function (similar to date_trunc) to Django ORM Queryset

我正在使用 timescaledb,它基本上只是 postgres 的扩展。它带有一个名为 time_bucket 的 SQL 函数。我想使用这个函数结合ORM生成如下查询:

SELECT
  time_bucket('1 minute', time) AS tb,
  AVG(s0) 
FROM measurements
WHERE
  time >= to_timestamp(1) AND
  time <= to_timestamp(2)
GROUP BY tb
ORDER BY tb ASC;

models.py:

class Measurement(models.Model):

    device_id = models.IntegerField(primary_key=True)
    time = models.DateTimeField()
    s0 = models.FloatField(blank=True, null=True)
    s1 = models.FloatField(blank=True, null=True)

到目前为止我的尝试:

class TimeBucket(Func):

    function = 'time_bucket'
    template = '%(function)s(\'{bucket_width}\', %(expressions)s)'.format(bucket_width='1 minute')


(Measurement.objects
    .values('time')
    .annotate(tb=TimeBucket('time'))
    .annotate(s_desc=Avg('s0'))
    .filter(
        time__gte=datetime.fromtimestamp(start),
        time__lte=datetime.fromtimestamp(end))
    .order_by('tb')
)

结果:

SELECT
  "measurements"."time",
  time_bucket('1 minute', "measurements"."time") AS "tb",
  (AVG("measurements"."s0")) AS "s_desc"
FROM "measurements"
WHERE (
  "measurements"."time" <= 2447-10-02 14:17:01+00:00 AND 
  "measurements"."time" >= 1970-01-01 00:00:01+00:00
)
GROUP BY "measurements"."time", time_bucket('1 minute', "measurements"."time")
ORDER BY "tb" ASC

如你所见,还有两个难看的点:

un-truncatedtime可以在过滤器before[=30]中使用 =] 第一个 annotate(...) 以后再也不会了。

必须在.values()(例如.values('tb'))任何聚合函数之前使用截断时间注释。

qs = (
    Measurement.objects
    .filter(...)
    .annotate(tb=TimeBucket('time'))
    .values('tb')
    .annotate(s_desc=Avg('s0'))
    .order_by('tb')
)

About alias in GROUP BY:别名一般只能在所有数据库的ORDER BY中使用,但只有特定的数据库才允许在GROUP BY或WHERE中使用(仅MySQL和Postgresql)。这可能是因为 GROUP BY 和 WHERE 子句在 SELECT 子句之前计算。这可以通过子查询绕过,但根本没有用。我确信每个现代数据库驱动程序的标准查询计划优化器都会重用 GROUP BY 中的辅助表达式,并且它从不重复计算函数。如果 SQL 是手动编写并由人类读取的,则别名很有用,但仅对某些后端通过别名在 Django ORM 编译器中实现它没有用。


编辑:此解决方案适用于 Django >= 1.11 并在 Django 3.0 中也经过测试。

(有关 Django 版本的信息不正确。我不记得 Django <=1.10 中的解决方案,但我确定它比 Django 1.11 中的更复杂。也许必须写一个额外的最终 .values('tb', 's_desc'))