I am trying to avoid an obscene amount of database queries in a Django app. In the app I am monotoring a number of suggestions (model: Suggestion) that can be voted for (model: Vote).
The Vote model does not store each individual vote. Instead the total number of votes for a suggestion are stored at regular intervals. A suggestion of "Better ice cream" could have "10 votes at 8:10", "12 votes at 8:20", "25 votes at 8:30", etc.
I have created a very ineffecient loop with some major n 1 issues to calculate the number of new votes per day per suggestion.
I am looking for a more efficient (probably single) queryset than the current ones for the same functionality. I know that I should probably create some kind of annotation by dates of votes on "suggestions" in views.py and then annotate that by my aggregate function that calculates the number of votes on each day, but I cannot figure out how to actually chain this together.
Here's my current working but very inefficient code:
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)
Template output:
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
CodePudding user response:
All you should need is values()
, annotate()
, and order_by()
to get the number of votes per day per suggestion. This here should work
Vote.objects.all() \
.values('timestamp__date', 'suggestion') \
.annotate(num_votes=Count('votes') \
.order_by('timestamp__date')
Although, your output example is not of number of votes per day per suggestion, and seems to be number of votes per day. That can be achieved by removing suggestion from the query like this:
Vote.objects.all() \
.values('timestamp__date') \
.annotate(num_votes=Count('votes') \
.order_by('timestamp__date')
CodePudding user response:
The following should get you all suggestions, dates and sum of votes in a values queryset
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})
Then in template use regroup to group all those results by the suggestion
{% 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>