Django уменьшает количество запросов (связь M2M со сквозной моделью)

Я хотел бы уменьшить количество подобных запросов. Вот мои модели:

class Skill(models.Model):
    name = models.TextField()

class Employee(models.Model):

    firstname = models.TextField()
    skills = models.ManyToManyField(Skill, through='SkillStatus')

    def skills_percentage(self):
        completed = 0
        total = 0

        for skill in self.skills.all().prefetch_related("skillstatus_set__employee"):
            for item in skill.skillstatus_set.all():
                if item.employee.firstname == self.firstname:
                    total += 1
                    if item.status:
                        completed += 1
        try:
            percentage = round((completed / total * 100), 2)
        except ZeroDivisionError:
            percentage = 0.0
        return f"{percentage} %"

class SkillStatus(models.Model):
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
    status = models.BooleanField(default=False)

Моя основная проблема связана с методом skills_percentage, я делаю слишком много запросов при вычислении указанного значения. Я уже немного улучшил ситуацию с prefetch_related, но в Django Debug Toolbar все еще есть лишние запросы. Что еще здесь можно сделать?

Я пробовал играть с различными комбинациями select_related и prefetch_related. Я думал о других вариантах для расчета skills_percentage, но они также требовались для многих запросов...

Заранее спасибо.

Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
О тренинге HTML JavaScript :HTML (язык гипертекстовой разметки) и CSS (каскадные таблицы стилей) - две основные технологии для создания веб-страниц....
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
Запуск PHP на IIS без использования программы установки веб-платформы
Запуск PHP на IIS без использования программы установки веб-платформы
Установщик веб-платформы, предлагаемый компанией Microsoft, перестанет работать 31 декабря 2022 года. Его закрытие привело к тому, что мы не можем...
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
0
0
56
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете попробовать так:

from django.db.models import Count, When, Case, Cast, FloatField

employees = Employee.objects.annotate(
        total=Count('skills',distinct=True),
        completed = Count('skills', filter=Q(skillstatus__status=True),distinct=True)
    ).annotate(
        percentage= Case(
            When(total=0, then=0.0),
            default=(Cast(
                F('completed')*100 / F('total'),
                output_field=FloatField()
            )
        )
    )
)

# usage

for employee in employees:
    print(employee.percentage)

# or 

employees.values('firstname', 'percentage')

Здесь я считаю навыки дважды, один раз без каких-либо условий для общего количества и второй раз с условием фильтра. Затем я аннотирую разделение завершенного/общего значения исходным набором запросов, преобразовывая его как FloatField.

Большое спасибо. Это немного уменьшило количество запросов, но «сотрудники» сами по себе являются набором запросов (а не числом). Или я что-то не так делаю?

Антон Ласько 17.02.2023 17:44

Вы можете получить доступ к этим значениям через цикл или набор запросов значений. Я добавлю несколько примеров в ответ

ruddra 17.02.2023 17:47

Здравствуйте, большое спасибо за поправку. Это избавило от лишних запросов, но, к сожалению, не у всех моих сотрудников результат верный. Похоже процент рассчитывается неправильно (может быть 1 или 0). Проблема где-то в "завершенном" значении... Я уже изменил "filter=Q(skills__status=True)" на "filter=Q(skills__skillstatus__status=True)", но что-то еще есть

Антон Ласько 20.02.2023 09:55

Исправлено с небольшой модификацией)employees = Employee.objects.annotate( total=Count('skillstatus', Different=True), Completed=Count('skillstatus', filter=Q(skillstatus__status=True), Different=True), ) .annotate (процент = регистр (когда (всего = 0, затем = 0,0), по умолчанию = (приведение (F («завершено») * 100 / F («всего»), output_field = FloatField () ) )) )

Антон Ласько 20.02.2023 10:52

Вы можете использовать функции агрегации, предоставляемые Django ORM. Это может помочь сократить количество запросов, необходимых для подсчета количества выполненных и общих навыков для сотрудника.

Использование выражений F() и annotate() позволяет нам выполнять вычисления в одном запросе без необходимости отдельного цикла для связанных объектов Skill.

from django.db.models import Count, F

class Employee(models.Model):
    ...

    def skills_percentage(self):
        counts = self.skills.annotate(
            completed_count=Count('skillstatus', filter=Q(skillstatus__status=True, skillstatus__employee=self)),
            total_count=Count('skillstatus', filter=Q(skillstatus__employee=self)),
        ).aggregate(
            completed_count_sum=Sum('completed_count'),
            total_count_sum=Sum('total_count'),
        )

        completed = counts['completed_count_sum'] or 0
        total = counts['total_count_sum'] or 0
        try:
            percentage = round((completed / total * 100), 2)
        except ZeroDivisionError:
            percentage = 0.0
        return f"{percentage} %"

Это определенно улучшило ситуацию, спасибо большое :) Теперь осталось всего 6 похожих запросов (было 9 на 3 сотрудников). Значит, вообще не нужно использовать prefetch_related?

Антон Ласько 17.02.2023 13:00

Вы можете использовать prefetch_related, чтобы еще больше сократить количество запросов, если они связаны с доступом к объектам Skill, связанным с экземплярами Employee. prefetch_related можно использовать для извлечения всех связанных объектов в одном запросе, что может быть более эффективным, чем извлечение их по одному в цикле. Однако имейте в виду, что prefetch_related может быть дорогостоящим с точки зрения использования памяти, поскольку он одновременно извлекает все связанные объекты в памяти. Поэтому вы должны использовать его разумно и только тогда, когда это имеет смысл делать в зависимости от конкретного варианта использования.

Leli Michael 17.02.2023 14:03

И есть ли способ объединить расчет «завершено» и «всего» в 1 набор запросов? Я пытался сделать это с помощью объекта F, но это не мой первый большой проект Django :)

Антон Ласько 17.02.2023 14:55

Да, можно объединить вычисление «завершено» и «всего» в один набор запросов, используя выражения Django F().

Leli Michael 17.02.2023 19:30

Другие вопросы по теме