Python: избавление от вложенных циклов при доступе к базе данных (Models? View?) - PullRequest
1 голос
/ 22 марта 2019

Я пытаюсь решить проблемы с производительностью на веб-сайте, основанном на Django, с очень небольшими знаниями синтаксиса Django и Python. Кажется, я правильно определил проблему. Кажется, я также знаю, что делать дальше, но не могу понять синтаксис Python / Django, чтобы все работало.

class Company(models.Model):
    name = models.CharField(max_length=100)
    bic = models.CharField(max_length=100, blank=True)

    def get_order_count(self):
        return self.orders.count()

    def get_order_sum(self):
        total_sum = 0
        for contact in self.contacts.all():
            for order in contact.orders.all():
                total_sum += order.total
        return total_sum

class Contact(models.Model):
    company = models.ForeignKey(
        Company, related_name="contacts", on_delete=models.DO_NOTHING)
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100, blank=True)

    def get_order_count(self):
        return self.orders.count()

class Order(models.Model):
    order_number = models.CharField(max_length=100)
    company = models.ForeignKey(Company, related_name="orders", on_delete=models.DO_NOTHING)
    contact = models.ForeignKey(Contact, related_name="orders", on_delete=models.DO_NOTHING)
    total = models.DecimalField(max_digits=18, decimal_places=9)
    order_date = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return "%s" % self.order_number

Я догадываюсь, что проблемы с производительностью вызваны вложенным циклом в get_order_sum. И мое решение вполне понятно: вложенные «fors» должны быть заменены простой командой, которая использует агрегирование и использует собственную эффективную внутреннюю функциональность SQL базы данных. Поэтому, на мой взгляд, решение должно выглядеть примерно так:

return self.contacts.all().orders.all().aggregate(Sum('total'))

Проблема в том, что я не могу понять, как правильно написать то, что я хочу, чтобы Django / Python делал. Пожалуйста, помогите мне!

Или я ошибаюсь и проблема (или ее часть) в моем View-коде?

<table>
    <tr>
        <th>Name</th>
        <th>Order Count</th>
        <th>Order Sum</th>
        <th>Select</th>
    </tr>
    {% for company in company_list|slice:":100" %}
    <tr>
        <td>{{ company.name }}</td>
        <td>{{ company.orders.count }}</td>
        <td>{{ company.get_order_sum|floatformat:2 }}</td>
        <td><input type="checkbox" name="select{{company.pk}}" id=""></td>
    </tr>
    {% for contact in company.contacts.all %}
    <tr>
        <td>- {{ contact.first_name }} {{ contact.last_name }}</td>
        <td>Orders: {{ contact.orders.count }}</td>
        <td>&nbsp;</td>
        <td>&nbsp;</td>
    </tr>
    {% endfor %}
    {% endfor %}
</table>

Буду также признателен за любые другие советы и мнения о том, как можно улучшить этот код (особенно из POV производительности).

Ответы [ 2 ]

1 голос
/ 22 марта 2019

Ключ не в том, чтобы иметь избыточные запросы к базе данных (это не то же самое, что иметь как можно меньше), и, как вы упомянули, чтобы база данных работала, это хорошо.

На практике, а) все необходимые данные должны быть в company_list к тому времени, когда они попадут в ваш шаблон, чтобы у вас не было запросов к базе данных, отправленных из циклов в вашем шаблоне, и б) company_list должны быть заполнены данными

from django.db.models import Prefetch, Sum, Count

contacts_with_orders = Prefetch(
    'contacts',
    queryset=Contact.objects.annotate(order_count=Count('orders'))
)

company_list = (Company.objects
    .prefetch_related(contacts_with_orders)
    .annotate(order_sum=Sum('orders__total'),
              order_count=Count('orders'))
)

Теперь вы можете делать свои циклы и получать доступ ко всем необходимым данным без каких-либо дополнительных запросов:

for company in company_list:
    company.name
    company.order_sum
    company.order_count
    for contact in company.contacts.all():
        contact.first_name
        contact.order_count

Хотя это должно быть на несколько порядков быстрее, чем раньше,это все еще довольно обременительно.Существует также еще несколько возможностей для оптимизации: при необходимости вы можете немного больше сбривать, запрашивая только нужные столбцы вместо полных строк и возвращая словари вместо объектов.См. only () , values ​​() и values_list () и параметр to_attr в режиме предварительной выборки.

1 голос
/ 22 марта 2019

Как вы уже догадались, ваша проблема с производительностью, скорее всего, вызвана методом get_order_sum . Вы запускаете запрос, чтобы получить все контуры компании, затем для каждого контакта вы запускаете запрос, чтобы получить заказчика этого контакта. Вы можете найти эту сумму с помощью одного запроса, например, в Django:

from django.db.models import Sum
def get_order_sum(self):
    return self.contacts.aggregate(order_sum=Sum('orders__total')).get('order_sum')

Обратите внимание, что агрегатная функция возвращает словарь в следующем формате:

{
    'order_sum': 123
}
...