Python Web框架Django是构建Performant Web应用程序的绝佳选择。 Disqus,Pinterest和Instagram都建在Django基金会上。 Django提供了简单,灵活性,可靠性,可伸缩性以及构建从管理员到身份验证到ORM的Web应用所需的所有工具。
但是,这种电池包括方法不利。它可以从开发人员那里隐藏应用程序的许多真正复杂性。 Django Orm是一个很好的例子。 Django Orm从开发人员那里抽象了数据库查询的复杂性,但是这样做,掩埋了一些可能正在减慢应用程序或引起其他性能问题的问题。
Identifying and troubleshooting these inefficiencies然后成为Django工作流程的重要组成部分。在这里,我们想带您了解这些问题如何表现,如何找到它们以及如何开始修复它们。
django orm及其问题
django orm,或对象相关映射是Django的功能,它允许您以与python对象进行交互的方式与数据库(如mySQL,PostgreSql或sqlite)进行交互。
ORM允许您在Django(称为模型)中创建一个Python类,然后将其转换为数据库表。这些模型的实例表示表中的行。 Django Orm提供了SQL上的抽象,这意味着您可以使用Python代码执行创建,读取,更新和删除(CRUD)操作,而不是编写RAW SQL查询。
例如,如果您在Django中有一个称为文章的模型,并且想从数据库中检索所有文章,则将编写Article.Objects.all()。
您定义了这样的模型:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
published_date = models.DateTimeField()
author = models.ForeignKey(Author, on_delete=models.CASCADE)
然后,您可以使用此模型执行CRUD操作,例如创建记录:
# Create an Author
author = Author.objects.create(name="John Doe")
# Create an Article
article = Article.objects.create(
title="Django ORM Tutorial",
content="This is a tutorial on Django ORM...",
published_date=datetime.datetime.now(),
author=author,
)
然后,您可以通过获取所有记录,或者使用过滤器来检索记录,或者仅选择所需的记录:
# Get all articles
articles = Article.objects.all()
# Get a single article by id
article = Article.objects.get(id=1)
# Get all articles written by a specific author
johns_articles = Article.objects.filter(author__name="John Doe")
django orm将其转化为幕后适当的SQL查询。例如,Artist.Objects.get(ID = 1)查询将变为:
SELECT * FROM article WHERE id = 1 LIMIT 1;
Django ORM的主要优点是它是数据库 - 敏捷。您可以将数据库后端切换为代码的最小更改,因为Django Orm会考虑将Python代码转换为数据库的适当SQL。
这显然非常方便。开发人员不必为他们的所有桌子手动编码SQL查询,甚至不需要创建桌子。这是在Python语言中提供的。
但强大的力量造成了巨大的责任。上面的Article.objects.filter(author__name="John Doe")
查询可以变成:
SELECT * FROM article INNER JOIN author ON article.author_id = author.id WHERE author.name = 'John Doe';
在SQL中加入可能很棘手。如果您没有正确地索引桌子,或者只有很大的桌子,那么连接将很慢(您必须穿越两个桌子)。问题不是Django Orm使用的加入加入是数据库查询的组成部分。 。当查询开始放慢速度时,此抽象使故障排除更加复杂。
效率低下的查询可能会对Django应用程序的性能产生一些负面影响:
- 增加了服务器负载:如果特定查询效率低下并且多次运行,它可能会在数据库服务器上添加不必要的负载,可能会减慢其他过程,甚至在极端情况下造成崩溃。
- 加载时间增加:执行效率低下的查询需要更长的时间,这可能会减慢网页的响应时间,从而导致用户体验差。
- 数据过载:效率低下的查询可能比必要的数据(过度提取)获取更多的数据,从而导致内存使用量和处理时间较慢。
这是应用程序性能监视工具可以帮助的地方。这些工具可以在您的应用程序中寻找性能问题,并将它们不仅与代码联系起来,还将实际执行的基础查询联系起来。因此,您可以看到您的实际查询和性能,并根据需要调整ORM代码。
Here we’ll use Scout APM to找到三个常见的ORM问题n+1查询,慢速查询和过度提取,然后如何修复它们。
查找增加服务器负载的N+1个查询
The "N+1" query problem加载父对象然后将其相关子对象加载到单独的查询中时,就会发生。对于Django,这通常会在循环中访问相关字段时会发生这种情况,从而导致Django执行一个查询以获取父对象,然后对每个父级的其他查询来获取其子对象(因此“ n+1”)。这导致许多不必要的数据库命中,并且可以大大减慢应用程序的速度。
这是django中如何出现n+1查询问题的一个示例:
假设您有一个博客应用程序,上面有两种模型,博客和条目。博客可以有多个条目。
class Blog(models.Model):
name = models.CharField(max_length=100)
class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=255)
现在,假设您要显示所有条目的列表以及每个条目所属的博客的名称。您可能会写这样的观点:
def entries(request):
entries = Entry.objects.all()
return render(request, 'entries.html', {'entries': entries})
然后在模板(entries.html)中,您可能有类似的东西:
{% for entry in entries %}
<p>{{ entry.headline }} - {{ entry.blog.name }}</p>
{% endfor %}
在这种情况下,Django的ORM将首先执行查询以获取所有条目。然后,对于每个条目,访问entry.blog.name时,将提出一个新的查询来获取相关的博客对象。因此,如果您有N条目,最终您将进行n+1个查询(1个获取所有条目,以及n n以获取每个条目相关的博客)。这是n+1查询问题。
这对Django Orm不一定是显而易见的,您可能会认为您只是在进行单个查询并检索博客。 。
在Scout APM中,n+1查询问题将在您的仪表板中突出显示:
在此示例中,我们可以看到该应用程序在此视图中进行了200个查询。然后,我们可以挖掘到端点,以了解有关特定查询的更多信息:
在这里,我们可以看到所有顺序调用以及引起问题的基础SQL查询。 core_author.name字段可能是我们一次又一次致电的另一个作者或人表的外键(加上1个呼叫core_author表)。
为了解决此问题,django提供了诸如select_reced和prefetch_recy的方法,您可以用来在同一查询中获取相关对象。
select_reled():这是一个性能助推器,使用SQL Join产生一个更复杂的查询。当您知道您需要检索的每个主要对象的相关对象时,在单个查询中获取“一对多”和“一对一”相关对象非常有用。例如,在一个博客应用程序中,输入模型具有博客的外键,entry.objects.select_realated('blog')将缓存相关的博客对象,避免访问entern.blog时避免其他数据库查询。
prefetch_realated():这类似于select_reced,但它在“多对多”和“多一对一”的关系上起作用,并且对每种关系进行单独查找,这可能会导致比Select_Reced的查询更多,但更少,但更少总的来说,如果相关表很大。例如,如果您的博客模型与标签模型有多一关系,blog.objects.prefetch_realated('tags')将一口气获取每个博客的所有相关标签对象。
在此示例中,您可以使用类似的内容修改视图:
def authors(request):
authors = Core_Authors.objects.select_related(‘name’).all()
return render(request, authors.html', {‘authors’: authors})
这将导致单个查询与SQL连接以获取核心作者及其相关名称,从而消除了N+1问题。
查找增加负载时间的慢速查询
n+1个查询是唯一可以降低应用程序性能的唯一优化的查询。根本不考虑如何获取数据也会引起问题。同样,django orm加剧了这个问题。获取数据非常容易,所以为什么不获取所有内容!
在SQL中,通常认为在查询中使用Select *(检索所有数据列)的糟糕习惯,但是如上所述,这实际上是Artist.Objects.all()或Article.Objects.get(ID = 1)正在做。在ORM隐藏的引擎盖下,您正在抓取给定条目的所有数据。
我们可以看到Scout APM中的外观。这是一个来自此问题的不同示例。我们可以在仪表板上看到一个查询几乎要返回一秒钟:
RefresHarticleSelect查询很慢。当我们单击它时,我们可以看到原因:
哦,男孩。对于我们正在查询的每篇文章,我们都在返回每一列数据。实际上,这个应用程序只需要其中一些字段。这是因为查询是:
class RefreshViewSet(viewsets.ModelViewSet):
serializer_class = RefreshSerializer
def get_queryset(self):
unique_id = self.kwargs['unique_id']
return Article.objects.filter(unique_id=unique_id).filter(refresh=1)
我们有效地使用选择 *。有了这个洞察力,我们可以重构此代码仅使用():
Article.objects.filter(unique_id=unique_id, refresh=1).only('title', 'url')
唯一的()仅需要来自模型的某些字段时。这有助于节省内存并减少数据库上的负载,仅在指定的字段中拉出而不是从模型中的所有字段。
查找过度提取,增加内存膨胀
上面的问题是当您检索比所需的更多数据时过度提取的一个很好的例子。这不仅是慢的,还可以导致memory bloat。
内存膨胀是指应用程序使用比必要多的内存更多的情况,通常是由于数据处理效率低下的情况,例如将更多的数据加载到内存中,或者在不再需要时无法释放内存。它可能导致绩效降低,基础设施成本增加以及在极端情况下,申请崩溃或放缓。
Scout APM告诉我们,我们还在此应用程序中看到内存膨胀:
我们的视图分配了100MB的内存。这源于与上述相同的查询,但对此有一个更微妙的问题。当我们编写这样的ORM语句时:
Article.objects.filter(unique_id=unique_id).filter(refresh=1)
我们不仅要检索所有数据,而且还在检索整个查询集。检索完整的模型实例比检索一部分字段更多的是内存和CPU密集型,尤其是在模型具有大量字段的情况下,或者某些字段包含大量数据。
我们可以使用上面的唯一()选项来解决此问题。但是,这也返回一个QuerySet,只是一个较小的QuerySet,仅包括您想要的字段。我们可以在此处使用的另一个选项是值()或values_list()方法仅选择查询中的特定字段:
Article.objects.filter(unique_id=unique_id, refresh=1).values('title', 'url')
这将返回一个列表(或带有values_list()的元组)而不是querySet,因此可以更具memory效率。
了解您的疑问
上面修复程序的一个有趣元素是它们也具有权衡。如果您选择仅使用()或values()来减少内存足迹,那么如果您确实需要其他字段,则现在需要更多数据库调用。
这就是为什么了解Django Orm正在发生的事情的原因。该出色的工具将复杂的查询减少到Python代码的单行。但是查询的复杂性并没有降低。它仍然在那里。
因此您需要看到它。诸如Scout APM之类的工具是为这种内省的。使用Django设置Scout APM很容易。您需要做的就是安装软件包,并添加更改一些设置,并且数据将立即开始管道。您可以在我们的documentation中找到更多。
最终,有效的Django orm使用的关键在于便利和性能的谨慎平衡 - 了解您的查询,监视其影响并使用正确的工具,例如Scout APM。这种意识,再加上Django Orm的力量,将使您能够建立高效,可扩展和可维护的应用程序。