【问题标题】:Count number of objects by date in daterange按日期计算日期范围内的对象数
【发布时间】:2018-01-17 09:02:08
【问题描述】:

在一个 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() 等),但我总是遇到同样的问题:我没有包含简单日期值的字段,我只有 start 和结束日期(在事件中)和出发和到达日期(在参与中),所以我找不到按日期对结果分组的方法。

【问题讨论】:

  • 也许你也应该用python 标记你的问题。那会引起更多的关注。此外,django-orm 似乎比 orm 更合适。
  • 谢谢,我已经按照你的建议做了

标签: python django performance django-orm


【解决方案1】:

我同意当前方法的成本很高,因为您每天都在重新查询数据库以查找您之前已检索到的参与者。相反,我会通过对数据库进行一次性查询来获取参与者,然后使用该数据填充您的结果数据结构来解决此问题。

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

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

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 代码并不太复杂,对于甚至有数十万人的事件应该相当容易处理。

【讨论】:

  • 这是一个非常优雅和聪明的解决方案。我确实测试了它,它完全符合我的需要。在执行了一些优化(与此处未提及的某些特定性相关)之后,您的解决方案需要大约相同的时间来执行,但会大大减少查询数量(特别是对于长事件)。我太专注于如何仅使用 SQL 来很好地执行此操作,但是添加一些 Python 逻辑也可以很好地工作。谢谢!
【解决方案2】:

几天前我看到了这个问题,并对其表示赞赏,因为它写得很好,问题也很有趣。最后我找到了一些时间来解决它。

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]

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

【讨论】:

  • 是的,我也喜欢你的解决方案。我在我的模型中使用了很多缓存的属性,并且将事件的日期列表和每次参与的列表放在一起是有意义的,因为这是我以后可能会重复使用的信息。我实现并检查了呈现页面所需的时间和查询数量。这相当于Rohan的解决方案。谢谢!
  • @Antwane 在性能方面,该解决方案可以与 Rohan 的解决方案相媲美,但无法击败它。你不能低于 1 个数据库命中,这个限制不能被打破。但是,我强烈建议您将业务逻辑和所有数据库操作放在模型层中。请检查编辑以查看如何扩展解决方案的示例。
猜你喜欢
  • 2016-05-18
  • 1970-01-01
  • 2011-02-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-12
相关资源
最近更新 更多