聚合分组注释

时间:2017-01-27 05:38:20

标签: django django-1.9

我希望将每天的所有活动持续时间相加。这是我的模特:

class Event(models.Model):
    start = models.DateTimeField()
    end = models.DateTimeField()

示例数据:

import datetime
from random import randint

for i in range(0, 1000):
    start = datetime.datetime(
        year=2016,
        month=1,
        day=randint(1, 10),
        hour=randint(0, 23),
        minute=randint(0, 59),
        second=randint(0, 59)
    )
    end = start + datetime.timedelta(seconds=randint(30, 1000))
    Event.objects.create(start=start, end=end)

我可以像每天一样获得事件计数: (我知道extra很糟糕,但我目前正在使用1.9。当我升级时,我会转而使用TruncDate

Event.objects.extra({'date': 'date(start)'}).order_by('date').values('date').annotate(count=Count('id'))

[{'count': 131, 'date': datetime.date(2016, 1, 1)},
 {'count': 95, 'date': datetime.date(2016, 1, 2)},
 {'count': 99, 'date': datetime.date(2016, 1, 3)},
 {'count': 85, 'date': datetime.date(2016, 1, 4)},
 {'count': 87, 'date': datetime.date(2016, 1, 5)},
 {'count': 94, 'date': datetime.date(2016, 1, 6)},
 {'count': 97, 'date': datetime.date(2016, 1, 7)},
 {'count': 111, 'date': datetime.date(2016, 1, 8)},
 {'count': 97, 'date': datetime.date(2016, 1, 9)},
 {'count': 104, 'date': datetime.date(2016, 1, 10)}]

我可以注释添加持续时间:

In [3]: Event.objects.annotate(duration=F('end') - F('start')).first().duration
Out[3]: datetime.timedelta(0, 470)

但我无法弄清楚如何对这个注释求和,就像我可以计算事件一样。我已经尝试过以下操作,但我的KeyError时间为{#1}}。

Event.objects.annotate(duration=F('end') - F('start')).extra({'date': 'date(start)'}).order_by('date').values('date').annotate(total_duration=Sum('duration'))

如果我将duration添加到values子句,则它不再按日期分组。

这是否可以在单个查询中进行并且不向模型添加持续时间字段?

1 个答案:

答案 0 :(得分:2)

我正准备写一个Django ORM不支持这个问题的答案。是的,然后我花了一个小时来解决这个问题(除了在开始写这个答案之前花了1.5小时),但事实证明,Django确实支持它。而且没有黑客攻击。好消息!

import datetime as dt

from django.db import models
from django.db.models import F, Sum, When, Case
from django.db.models.functions import TruncDate

from app.models import Event

a = Event.objects.annotate(date=TruncDate('start')).values('date').annotate(
    day_duration=Sum(Case(
        When(date=TruncDate(F('start')), then=F('end') - F('start')),
        default=dt.timedelta(), output_field=models.DurationField()
    ))
)

一些初步测试(希望)证明这些东西确实能满足你的要求。

In [71]: a = Event.objects.annotate(date=TruncDate('start')).values('date').annotate(day_duration=Sum(Case(
    ...:         When(date=TruncDate(F('start')), then=F('end') - F('start')),
    ...:         default=dt.timedelta(), output_field=models.DurationField()
    ...:     ))
    ...: )

In [72]: for e in a:
    ...:     print(e)
    ...:     
{'day_duration': datetime.timedelta(0, 41681), 'date': datetime.date(2016, 1, 10)}
{'day_duration': datetime.timedelta(0, 46881), 'date': datetime.date(2016, 1, 3)}
{'day_duration': datetime.timedelta(0, 48650), 'date': datetime.date(2016, 1, 1)}
{'day_duration': datetime.timedelta(0, 52689), 'date': datetime.date(2016, 1, 8)}
{'day_duration': datetime.timedelta(0, 45788), 'date': datetime.date(2016, 1, 5)}
{'day_duration': datetime.timedelta(0, 49418), 'date': datetime.date(2016, 1, 7)}
{'day_duration': datetime.timedelta(0, 45984), 'date': datetime.date(2016, 1, 9)}
{'day_duration': datetime.timedelta(0, 51841), 'date': datetime.date(2016, 1, 2)}
{'day_duration': datetime.timedelta(0, 63770), 'date': datetime.date(2016, 1, 4)}
{'day_duration': datetime.timedelta(0, 57205), 'date': datetime.date(2016, 1, 6)}

In [73]: q = dt.timedelta()

In [74]: o = Event.objects.filter(start__date=dt.date(2016, 1, 7))

In [75]: p = Event.objects.filter(start__date=dt.date(2016, 1, 10))

In [76]: for e in o:
    ...:     q += (e.end - e.start)

In [77]: q
Out[77]: datetime.timedelta(0, 49418) # Matches 2016.1.7, yay!

In [78]: q = dt.timedelta()

In [79]: for e in p:
    ...:     q += (e.end - e.start)

In [80]: q
Out[80]: datetime.timedelta(0, 41681) # Matches 2016.1.10, yay!

NB!这适用于1.9版本,我不认为您可以使用早期版本执行此操作,因为缺少TruncDate函数。在1.8之前,你当然也没有CaseWhen件事。