Сумма средних значений необработанного запроса

У меня есть следующий код, который мне нужно оптимизировать:

Вот эти модели:

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() для получения даты+времени.

Вернуться на верх