Использование prefetch_related и агрегации для предотвращения проблемы n+1 в запросах к базе данных Django для модели с данными временного ряда
Я пытаюсь избежать неприличного количества запросов к базе данных в приложении Django. В приложении я монотизирую ряд предложений (модель: Suggestion), за которые можно голосовать (модель: Vote).
Модель Vote не хранит каждый отдельный голос. Вместо этого общее количество голосов за предложение хранится через регулярные промежутки времени. Предложение "Лучшее мороженое" может иметь "10 голосов в 8:10", "12 голосов в 8:20", "25 голосов в 8:30" и т.д.
Я создал очень неэффективный цикл с некоторыми серьезными проблемами 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
Все, что вам нужно, это 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')
Ниже приведены все предложения, даты и сумма голосов в наборе значений
from django.db.models import Sum
from django.db.models.functions import TruncDate
def index(request):
suggestions = Suggestion.objects.annotate(
date=TruncDate('vote__timestamp')
).order_by(
'id', 'date'
).annotate(
sum=Sum('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>