按日期在daterange中计算对象数

时间:2018-01-17 09:02:08

标签: python django performance django-orm

在Django项目中,我定义了这些简化模型:

class People(models.Model):
    name = models.CharField(max_length=96)

class Event(models.Model):

    name = models.CharField(verbose_name='Nom', max_length=96)

    date_start = models.DateField()
    date_end = models.DateField()

    participants = models.ManyToManyField(to='People', through='Participation')

class Participation(models.Model):
    """Represent the participation of 1 people to 1 event, with information about arrival date and departure date"""

    people = models.ForeignKey(to=People, on_delete=models.CASCADE)
    event = models.ForeignKey(to=Event, on_delete=models.CASCADE)

    arrival_d = models.DateField(blank=True, null=True)
    departure_d = models.DateField(blank=True, null=True)

现在,我需要生成参与图:对于每个单一事件日,我想要相应的参与总数。 目前,我使用这个糟糕的代码:

def daterange(start, end, include_last_day=False):
    """Return a generator for each date between start and end"""
    days = int((end - start).days)
    if include_last_day:
        days += 1
    for n in range(days):
        yield start + timedelta(n)

class ParticipationGraph(DetailView):

    template_name = 'events/participation_graph.html'
    model = Event

    def get_context_data(self, **kwargs):

        labels = []
        data = []

        for d in daterange(self.object.date_start, self.object.date_end):
            labels.append(formats.date_format(d, 'd/m/Y'))
            total_participation = self.object.participation_set
                .filter(arrival_d__lte=d, departure_d__gte=d).count()
            data.append(total_participation)

        kwargs.update({
            'labels': labels,
            'data': data,
        })
        return super(ParticipationGraph, self).get_context_data(**kwargs)

显然,我在Event.date_startEvent.date_end之间的每一天都运行一个新的SQL查询。 有没有办法通过减少SQL查询次数来获得相同的结果(理想情况下,只有一个)?

我尝试了很多来自Django orm的聚合工具(values(),distinct()等)但我总是遇到同样的问题:我没有一个具有简单日期值的字段,我只有开始和结束日期(在事件中)以及出发和到达日期(在参与中),因此我找不到按日期对结果进行分组的方法。

2 个答案:

答案 0 :(得分:4)

我同意当前的方法很昂贵,因为您每天都要为之前检索过的参与者重新查询数据库。我会通过对数据库进行一次性查询来获取参与者,然后使用该数据填充结果数据结构来实现此目的。

我将对您的解决方案进行的一项结构性更改是,不是跟踪两个列表,其中每个索引对应于一天和参与,而是将数据聚合在将日期映射到参与者数量的字典中。如果我们以这种方式聚合结果,我们总是可以根据需要将其转换为最后的两个列表。

这是我的一般(伪代码)方法:

def formatDate(d):
    return formats.date_format(d, 'd/m/Y')

def get_context_data(self, **kwargs):

    # initialize the results with dates in question
    result = {}
    for d in daterange(self.object.date_start, self.object.date_end):
        result[formatDate(d)] = 0

    # for each participant, add 1 to each date that they are there
    for participant in self.object.participation_set:
        for d in daterange(participant.arrival_d, participant.departure_d):
            result[formatDate(d)] += 1

    # if needed, convert result to appropriate two-list format here

    kwargs.update({
        'participation_amounts': result
    })
    return super(ParticipationGraph, self).get_context_data(**kwargs)

在性能方面,两种方法都执行相同数量的操作。在你的方法中,对于每一天,d,你过滤每个参与者,p。因此,操作次数是O(dp)。在我的方法中,对于每个参与者,我每天都会参加(每天更差,d)。因此,它也是O(dp)。

您更喜欢我的方法的原因是您所指出的。它只访问数据库一次以检索参与者列表。因此,它较少依赖于网络延迟。它确实牺牲了通过python代码从SQL查询中获得的一些优势。但是,python代码并不是太复杂,对于甚至有数十万人的事件来说,它应该相当容易处理。

答案 1 :(得分:1)

前几天我看到了这个问题,并用一个upvote来表达这个问题,因为它写的很好而且问题非常有趣。最后,我找到了一些时间专门用于解决方案。

Django是模型 - 视图 - 控制器的变体,称为模型 - 模板 - 视图。因此,我的方法将遵循范式"胖模型和瘦控制器" (或翻译为符合Django"胖模型和瘦视图")。

以下是我将如何重写模型:

import pandas

from django.db import models
from django.utils.functional import cached_property


class Person(models.Model):
    name = models.CharField(max_length=96)


class Event(models.Model):
    name = models.CharField(verbose_name='Nom', max_length=96)
    date_start = models.DateField()
    date_end = models.DateField()
    participants = models.ManyToManyField(to='Person', through='Participation')

    @cached_property
    def days(self):
        days = pandas.date_range(self.date_start, self.date_end).tolist()
        return [day.date() for day in days]

    @cached_property
    def number_of_participants_per_day(self):
        number_of_participants = []
        participations = self.participation_set.all()
        for day in self.days:
            count = len([par for par in participations if day in par.days])
            number_of_participants.append((day, count))
        return number_of_participants


class Participation(models.Model):
    people = models.ForeignKey(to=Person, on_delete=models.CASCADE)
    event = models.ForeignKey(to=Event, on_delete=models.CASCADE)
    arrival_d = models.DateField(blank=True, null=True)
    departure_d = models.DateField(blank=True, null=True)

    @cached_property
    def days(self):
        days = pandas.date_range(self.arrival_d, self.departure_d).tolist()
        return [day.date() for day in days]

所有计算都放在模型中。依赖于数据库中存储的数据的信息可以 cached_property 获得。

让我们看一下Event的例子:

djangocon = Event.objects.create(
    name='DjangoCon Europe 2018',
    date_start=date(2018,5,23),
    date_end=date(2018,5,28)
)
djangocon.days
>>> [datetime.date(2018, 5, 23),
     datetime.date(2018, 5, 24),
     datetime.date(2018, 5, 25),
     datetime.date(2018, 5, 26),
     datetime.date(2018, 5, 27),
     datetime.date(2018, 5, 28)]

我使用pandas来生成日期范围,这可能对您的应用程序来说太过分了,但它具有很好的语法并且有助于演示目的。您可以用自己的方式生成日期范围 要获得此结果,只有一个查询。 days可用于任何其他字段 我在Participation做的同样的事情,这里有一些例子:

antwane = Person.objects.create(name='Antwane')
rohan = Person.objects.create(name='Rohan Varma')
cezar = Person.objects.create(name='cezar')

他们都希望在2018年访问DjangoCon Europe,但并非所有人都参加了所有这些活动:

p1 = Participation.objects.create(
    people=antwane,
    event=djangocon,
    arrival_d=date(2018,5,23),
    departure_d=date(2018,5,28)
)
p2 = Participation.objects.create(
    people=rohan,
    event=djangocon,
    arrival_d=date(2018,5,23),
    departure_d=date(2018,5,26)
)
p3 = Participation.objects.create(
    people=cezar,
    event=djangocon,
    arrival_d=date(2018,5,25),
    departure_d=date(2018,5,28)
)

现在我们想看看活动每天有多少参与者。我们也跟踪SQL查询的数量。

from django.db import connection
djangocon = Event.objects.get(pk=1)
djangocon.number_of_participants_per_day
>>> [(datetime.date(2018, 5, 23), 2),
     (datetime.date(2018, 5, 24), 2),
     (datetime.date(2018, 5, 25), 3),
     (datetime.date(2018, 5, 26), 3),
     (datetime.date(2018, 5, 27), 2),
     (datetime.date(2018, 5, 28), 2)]

connection.queries
>>>[{'time': '0.000', 'sql': 'SELECT "participants_event"."id", "participants_event"."name", "participants_event"."date_start", "participants_event"."date_end" FROM "participants_event" WHERE "participants_event"."id" = 1'},
    {'time': '0.000', 'sql': 'SELECT "participants_participation"."id", "participants_participation"."people_id", "participants_participation"."event_id", "participants_participation"."arrival_d", "participants_participation"."departure_d" FROM "participants_participation" WHERE "participants_participation"."event_id" = 1'}]

有两个查询。第一个获取对象Event,第二个获取事件每天的参与者数量。

现在,您可以根据自己的意愿在视图中使用它。并且由于缓存的属性,您不需要重复数据库查询来获得结果。

您可以遵循相同的原则,也可以添加属性以列出活动每一天的所有参与者。它可能看起来像:

class Event(models.Model):
    # ... snip ...
    @cached_property
    def participants_per_day(self):
        participants  = []
        participations = self.participation_set.all().select_related('people')
        for day in self.days:
            people = [par.people for par in participations if day in par.days]
            participants.append((day, people))
        return participants

    # refactor the number of participants per day
    @cached_property
    def number_of_participants_per_day(self):
        return [(day, len(people)) for day, people in self.participants_per_day]

我希望你喜欢这个解决方案。