Использование prefetch_related и агрегации, чтобы избежать n 1 проблемы с запросами базы данных Django для модели с данными временных рядов

#python #django #django-queryset #aggregation

Вопрос:

Я пытаюсь избежать непристойного количества запросов к базе данных в приложении Django. В приложении я повторяю ряд предложений (модель: Предложение), за которые можно проголосовать (модель: Голосование).

Модель голосования не хранит каждый отдельный голос. Вместо этого общее количество голосов за предложение сохраняется через регулярные промежутки времени. Предложение «Лучшее мороженое» может иметь «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
 

Ответ №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>
 

Комментарии:

1. Спасибо, это привело меня туда. Я предложил отредактировать, чтобы отразить, что я сохраняю не количество добавленных голосов за метку времени в модели голосования, а общее количество голосов за метку времени, поэтому вместо Sum(‘vote__votes’), Max(‘vote__votes’) — Min(‘vote__votes’) должен быть вычислен.

2. @Morten Ah, Голосование содержит общее количество голосов или моментальный снимок во времени количества голосов?

3. Моментальный снимок общего количества голосов за предложение во времени. Если число голосов в 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')
 

Комментарии:

1. Спасибо, но я боюсь, что использование Count не работает с моделью данных, но, возможно, мой вопрос должен был быть более точным. Модель голосования хранит общее количество голосов в определенные временные метки «Лучшее мороженое» может иметь «ОБЩЕЕ количество голосов 10 в 8: 10», «ОБЩЕЕ количество голосов 12 в 8:20», «ОБЩЕЕ количество голосов 25 в 8:30» и т.д.