Django prefetch_related 和循环内多个查询集的性能优化

Django prefetch_related and performance optimisation with multiple QuerySets within loops

实际上我在循环中有多个查询,我只是不满意。我想知道是否有人具有 prefetch_related 和其他 Django 查询构造优化方面的专业知识,能够帮助我解决这个问题。

我开始于:

users = User.objects.filter(organisation=organisation).filter(is_active=True)

然后,我从某个日期“start_date”:

开始循环所有天
for date in (start_date + datetime.timedelta(n) for n in range((datetime.datetime.now().replace(tzinfo=pytz.UTC) - start_date).days + 1)):

然后我在这个循环中过滤了上述 users

的子集
for date in (start_date + datetime.timedelta(n) for n in range((datetime.datetime.now().replace(tzinfo=pytz.UTC) - start_date).days + 1)):
  for user in users.filter(created_date__lte=date).iterator():

好的,那么首先,有什么办法可以优化这个吗?

是什么让一些铁杆 Django 用户失去了束缚,我在另一个循环中完成了上述所有操作!

for survey in Survey.objects.all().iterator():
   for date in (start_date + datetime.timedelta(n) for n in range((datetime.datetime.now().replace(tzinfo=pytz.UTC) - start_date).days + 1)):
      for user in users.filter(created_date__lte=date).iterator():

在最后一个循环中,我执行了一个最终查询过滤器:

survey_result = SurveyResult.objects.filter(survey=survey, user=user, created_date__lte=date).order_by('-updated_date')[0]

我这样做是因为我觉得我需要让用户、调查和日期变量准备好进行过滤...

我已经开始考虑 prefetch_related 和 Prefetch 对象。我已经查阅了文档,但我似乎无法理解如何将其应用于我的情况。

实际上,查询花费的时间太长了。对于平均 1000 位用户、4 次调查和大约 30 天,此查询需要 1 分钟才能完成。

理想情况下,我想剃掉其中的 50%。任何更好的,我会非常高兴。我还希望减少数据库服务器上的负载,因为此查询可能 运行 跨不同组织多次。

任何有关如何在循环中的循环中组织此类可怕查询的专家提示都将不胜感激!

完整 "condensed" 最小可行代码段:


users = User.objects.filter(organisation=organisation).filter(is_active=True)

datasets = []

for survey in Survey.objects.all():
    data = []
    for date in (start_date + datetime.timedelta(n) for n in range((datetime.datetime.now().replace(tzinfo=pytz.UTC) - start_date).days + 1)):
        total_score = 0
        participants = 0

        for user in users.filter(created_date__lte=date):
             participants += 1
             survey_result = SurveyResult.objects.filter(survey=survey, user=user, created_date__lte=date).order_by('-updated_date')[0]
             total_score += survey_result.score

        # An average is calculated from the total_score and participants and append to the data array.:
        # Divide catches divide by zero errors.
        # Round will round to two decimal places for front end readability.
        data.append(
            round(
                divide(total_score, participants), 2
            )
        )

    datasets.append(data)

********* 附录:*********

所以,根据@dirkgroten 的回答,我目前 运行:

for survey in Survey.objects.all():

                results = SurveyResult.objects.filter(
                    user__in=users, survey=survey, created_date__range=date_range
                ).values(
                    'survey',
                    'created_date',
                ).annotate(
                    total_score=Sum('normalized_score'),
                    participants=Count('user'),
                    average_score=Avg('normalized_score'),
                ).order_by(
                    'created_date'
                )

                for result in results:
                    print(result)

因为我(“认为我”)需要对每个查询集进行调查细分。

我还有其他可用的优化吗?

您实际上可以组合查询并直接在查询中执行计算:

from django.db.models import Sum, Count, Avg
from django.utils import timezone
users = User.objects.filter(organisation=organisation).filter(is_active=True)
date_range = [start_date, timezone.now().date]  # or adapt end time to different time zone
results = SurveyResult.objects.filter(user__in=users, created_date__range=date_range)\
              .values('survey', 'created_date')\
              .annotate(total_score=Sum('score'), participants=Count('pk'))
              .order_by('survey', 'created_date')

这将按 surveycreated_date 对结果进行分组,并将 total_scoreparticipants 添加到每个结果中,例如:

[{'survey': 1, 'created_date': '2019-08-05', 'total_score': 54, 'participants': 20}, 
 {'survey': 1, ... } ... ]

我假设每个用户只有一个 SurveyResult,所以每个组中 SurveyResult 的数量就是参与者的数量。

请注意,Avg 也会立即为您提供平均分数,即假设每个用户只有一个可能的分数:

.annotate(average_score=Avg('score'))  # instead of total and participants

这应该可以减少 99.9% 的查询时间:-)

如果你想要数据集作为列表的列表,你只需这样做:

dataset = []
data = []
current_survey = None
current_date = start_date
for result in results
    if not result['survey'] == current_survey:
        # results ordered by survey, so if it changes, reset data
        if data: dataset.append(data)            
        data = []
        current_survey = result['survey']
    if not result['created_date'] == current_date:
        # results ordered by date so missing date won't be there later 
        # assume a daterange function to create a list of dates
        for date in daterange(current_date, result['created_date']):
            data.append(0)  # padding data
     current_date = result['created_date']
     data.append(result['average_score'])

结果将是一个列表列表:

dataset = [[0, 0, 10.4, 3.9, 0], [20.2, 3.5, ...], ...]

效率不高python,但是如果有几千个结果,这无论如何都会非常快,比执行更多的数据库查询要快得多。

编辑:由于created_date是DateTimeField,所以首先需要得到对应的日期:

from django.db.models.functions import TruncDate
results = SurveyResult.objects.filter(user__in=users, created_date__range=date_range)
               .annotate(date=TruncDate('created_date'))
               .values('survey', 'date')
               .annotate(average_score=Avg('score'))
               .order_by('survey', 'date')