使用 prefetch_related 和聚合来避免 Django 数据库查询具有时间序列数据的模型的 n+1 问题

Posted

技术标签:

【中文标题】使用 prefetch_related 和聚合来避免 Django 数据库查询具有时间序列数据的模型的 n+1 问题【英文标题】:Using prefetch_related and aggregations to avoid n+1 issue with Django database queries for model with time series data 【发布时间】:2021-12-25 12:23:20 【问题描述】:

我试图避免在 Django 应用程序中进行大量的数据库查询。在应用程序中,我正在监控一些可以投票的建议(模型:建议)(模型:投票)。

投票模型不存储每个单独的投票。相反,建议的总票数会定期存储。 “更好的冰淇淋”的建议可能有“8:10 10 票”、“8:20 12 票”、“8:30 25 票”等。

我创建了一个非常低效的循环,其中包含一些主要的 n+1 问题来计算每个建议每天的新投票数。

对于相同的功能,我正在寻找比当前查询集更高效(可能是单个)的查询集。我知道我可能应该通过views.py中“建议”的投票日期创建某种注释,然后通过计算每天投票数的聚合函数对其进行注释,但我无法弄清楚如何实际链接这个在一起。

这是我当前工作但效率非常低的代码:

models.py:

class Suggestion(models.Model):
    unique_id = models.CharField(max_length=10, unique=True)
    title = models.CharField(max_length=500)
    suggested_date = models.DateField()
​
class Vote(models.Model):
    suggestion = models.ForeignKey('Suggestion', on_delete=models.CASCADE)
    timestamp = models.DateTimeField()
    votes = models.IntegerField()

views.py:

def index(request):
    # Proces votes per day per suggestion
    suggestions = Suggestion.objects.prefetch_related('vote_set')
    votes_per_day_per_suggestion = 
    for suggestion in suggestions:
        votes_per_day_per_suggestion[suggestion.title] = 
        votes = suggestion.vote_set
        suggestion_dates = votes.dates('timestamp', 'day') # n+1 issue
        for date in suggestion_dates:
            date_min_max = votes.filter(timestamp__date=date).aggregate(votes_on_date=(Max('votes') - Min('votes'))) # n+1 issue
            votes_per_day_per_suggestion[suggestion.title][date] = date_min_max['votes_on_date']
    context['votes_per_day_per_suggestion'] = votes_per_day_per_suggestion
    return render(request, 'borgerforslag/index.html', context)

模板输出:

Better toilet paper (number of votes per day):
19. october 2021: 23
20. october 2021: 19
21. october 2021: 18
22. october 2021: 9
23. october 2021: 25
24. october 2021: 34
25. october 2021: 216

【问题讨论】:

【参考方案1】:

以下内容应为您提供值查询集中的所有建议、日期和投票总和

from django.db.models import Max, Min
from django.db.models.functions import TruncDate


def index(request):
    suggestions = Suggestion.objects.annotate(
        date=TruncDate('vote__timestamp')
    ).order_by(
        'id', 'date'
    ).annotate(
        sum=Max('vote__votes') - Min('vote__votes')
    )
    return render(request, 'borgerforslag/index.html', 'suggestions': suggestions)

然后在模板中使用 regroup 按建议对所有结果进行分组

% regroup suggestions by title as suggestions_grouped %

<ul>
% for suggestion in suggestions_grouped %
    <li> suggestion.grouper 
    <ul>
        % for date in suggestion.list %
          <li> date.date :  date.sum </li>
        % endfor %
    </ul>
    </li>
% endfor %
</ul>

【讨论】:

谢谢,这让我到了那里。我建议进行编辑以反映我没有在投票模型中保存每个时间戳的添加投票数,而是在某个时间时间戳记的投票总数,因此不是 Sum('vote__votes'),而是 Max('vote__votes' ) - 应计算 Min('vote__votes')。 @Morten Ah,Vote 包含一个运行总票数或票数时间快照? 建议的总票数的快照。如果 8:10 的投票数为 10,则总共有 10 人投票支持该建议。如果 8:20 的投票数为 13,则共有 13 人投票支持该建议。【参考方案2】:

您只需要values()annotate()order_by() 即可获得每个建议每天的投票数。这应该可以工作

Vote.objects.all() \
    .values('timestamp__date', 'suggestion') \
    .annotate(num_votes=Count('votes') \
    .order_by('timestamp__date')

虽然,您的输出示例不是每个建议每天的投票数,而似乎是每天的投票数。这可以通过从查询中删除建议来实现,如下所示:

Vote.objects.all() \
    .values('timestamp__date') \
    .annotate(num_votes=Count('votes') \
    .order_by('timestamp__date')

【讨论】:

谢谢,但恐怕使用 Count 不适用于数据模型,但也许我的问题应该更准确。投票模型在特定时间戳存储总票数“Better ice cream”可能有“10 TOTAL votes at 8:10”、“12 TOTAL votes at 8:20”、“25 TOTAL 8:30 投票”等

以上是关于使用 prefetch_related 和聚合来避免 Django 数据库查询具有时间序列数据的模型的 n+1 问题的主要内容,如果未能解决你的问题,请参考以下文章

django功能六

AttributeError:“Resume”对象没有“prefetch_related”属性

django- 在另一个 prefetch_related 中使用 prefetch_related

pythonのdjango select_related 和 prefetch_related()

转 实例具体解释DJANGO的 SELECT_RELATED 和 PREFETCH_RELATED 函数对 QUERYSET 查询的优化

实例具体解释Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化