Сумма средних значений необработанного запроса
У меня есть следующий код, который мне нужно оптимизировать:
Вот эти модели:
class Question(models.Model):
label = models.CharField(max_length=255, verbose_name='Question')
class Response(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
submit_date = models.DateTimeField()
int_val = models.IntegerField(null=True, blank=True)
class Plan(models.Model):
name = models.CharField(max_length=100)
questions = models.ManyToManyField(Question, through='PlanQuestion')
start_date = models.DateField(null=True)
completion_date = models.DateField(null=True)
class PlanQuestion(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
plan = models.ForeignKey(Plan, on_delete=models.CASCADE)
target_score = models.FloatField()
target_percentage = models.FloatField()
А вот неоптимизированный код:
plans = Plan.objects.filter(start_date__isnull=False, completion_date__isnull=False)
sum_of_averages = 0
total_plans = 0
for plan in plans:
plan_questions = plan.questions.through.objects.filter(plan=plan)
sum_of_scores = 0
total_plan_questions = 0
for plan_question in plan_questions:
cur_date = datetime.now().date()
start_date = plan.completion_date
end_date = start_date if start_date and (cur_date > start_date) else cur_date
query = """
SELECT id, COUNT(*) AS count, AVG(int_val) AS int_average
FROM Response WHERE question_id=%(question_id)s
AND DATE(submit_date) >= %(stard_date)s AND DATE(submit_date) <= %(end_date)s
"""
score = Response.objects.raw(query,params={'question_id': plan_question.question.id,
'start_date': plan_question.plan.start_date if plan_question.plan.start_date else end_date, 'end_date': end_date
})[0].int_average
score_percentage = ((score / plan_question.target_score) - 1) * 100 if score < plan_question.target_score else (score / plan_question.target_score) * 100
sum_of_scores += score_percentage
total_plan_questions += 1
question_avg = sum_of_scores / total_plan_questions if total_plan_questions else 0
sum_of_averages += question_avg
total_plans += 1
return sum_of_averages / total_plans if total_plans else 0
Как видно из приведенного выше кода, для каждого вопроса плана вычисляется балл, затем среднее значение, а затем вычисляется сумма среднего, что делает много запросов к базе данных. Я хочу уменьшить его настолько, насколько это возможно.
Я думал написать один запрос, который заменит весь код, возможно, с помощью джойнов, но я не знаю, как это сделать, так как в коде много условий. Пожалуйста, помогите.
Я также пытался добавить prefech_related, но это не дало никаких улучшений.
Это не имеет большого смысла:
SELECT id, COUNT(*) AS count, AVG(int_val) AS int_average
FROM Response
WHERE question_id=%(question_id)s
AND DATE(submit_date) >= %(stard_date)s
AND DATE(submit_date) <= %(end_date)s
Без GROUP BY, COUNT и AVG будут итоговыми для одного "question_id". Но тогда, если для каждой строки будет свой id, на какой id вы надеетесь?
OK, предполагая, что id удален, ему нужен этот составной индекс с колонками в таком порядке:
INDEX(question_id, submit_date)
Уберите INDEX(question_id), потому что он будет мешать.
Извините, но иногда производительность требует изменений.
Во-вторых... "for plan_question in plan_questions" подразумевает, что вы хотите, чтобы это выполнялось для каждого "вопроса"?
Тогда избавьтесь от цикла и выполняйте всю работу одновременно:
SELECT question_id, COUNT(*) AS count, AVG(int_val) AS int_average
FROM Response
WHERE question_id=%(question_id)s
AND DATE(submit_date) >= %(stard_date)s
AND DATE(submit_date) <= %(end_date)s
GROUP BY question_id
Это вернет по одной строке на вопрос; затем вы можете просмотреть набор результатов в цикле, чтобы получить результат.
Хорошие новости: Даже если вы не добавите вышеуказанный индекс, это будет работать лучше, чем то, что у вас есть сейчас.
Также... cur_date = datetime.now().date() можно убрать из кода приложения; вместо этого используйте просто CURDATE() в SQL для получения только даты или NOW() для получения даты+времени.