Python:访问数据库(模型?视图?)时摆脱嵌套的for循环

时间:2019-03-21 21:58:36

标签: python django django-models django-views

我试图在一个基于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做的事情。请帮帮我!

还是我弄错了,是我的查看代码中的问题(或其一部分)吗?

<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 个答案:

答案 0 :(得分:1)

您猜到,您的性能问题很可能是由 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
}

答案 1 :(得分:1)

关键是不要有过多的数据库查询(这与尽可能少地查询不一样),并且正如您所提到的,让数据库正常工作是很擅长的。

实际上,a)您需要的所有数据都应在到达模板之前company_list中,这样您就不会在模板的循环内发送数据库查询了; b) 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() and values_list()to_attr参数。