【译】Django 数据查询性能优化最佳实践

原文链接:http://bookofstranger.com/optimizing-django-orm-queries-for-best-performance/

Django 框架提供了众多的 ORM 方法,当一个 Model 创建后,你可以通过其 API 进行数据的创建、更新、检索与删除。当 Model 发生变化的时候可以通过迁移完成对数据库的操作。总的来说,Django 中的查询操作是非常简单和直观的,但是如果没有采用合适的写法的话也会造成性能问题。

通过采用下面的技巧,我将数据库的查询次数降低了 92%,查询的执行时间降低了 80%。我将会在后面的文章中详细解释与讨论这些技巧。

一. 理解 QuerySet 是如何工作的

一个非常重要的事实是:Django QuerySet是懒执行的,只有访问到对应数据的时候,才会去访问数据库。另外如果你再次读取查询到的数据,将不会触发数据库的访问。

代码示例

# 这里不会访问数据
blogs = Blog.objects.filter(category='django')     

# 这里需要访问数据,因此会执行数据库查询
if blogs:
    print 'yes'

# 再次读取,并不会访问数据库
randomBlog = blogs[0]

了解了 QuerySet 的查询原理后,下面是一些可以提高查询性能的技巧。

二. 使用 select_related 或者 prefetch_related

大多数 Model 都定义了外键关联。当进行关联查询的时候,如果编写不正确,Django 将会多次访问库进行查询,我们应该避免这种情况。尤其是当查询语句位于某个循环中的时候,会导致只需要执行一次的查询重复执行多次。

来举一个例子进行说明,我们需要查询一个含有外键的表,并且要访问外键的数据。其 Model 定义如下:

Class Blog(models.Model):
    title = models.CharField(max_length=32)
    description = models.CharField(max_length=255)
    author = models.ForeignKey(Author)

Class Author(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField(unique=True)
    city = models.CharField(max_length=32)

# 访问一次数据库
blog = Blog.objects.get(id=124)

# 再次访问数据库
author = blog.author

上面的代码访问了两次数据库,为了优化,可以通过 select_related 方法来查询外键关系,其实就是通过 join 来进行联结查询。我们可以将任何外键或者外键的列表传递给该方法,下面是使用示例:

# 访问数据库
blog = Blog.objects.select_related('author').get(id=124)

# 这里不会再次访问数据库,在之前的查询中已经取到了对应的数据
author = blog.author

除了 select_related() 方法之外还有与之类似的 prefetch_realated() 方法,这两个方法目的是一样的,但是实现策略有所不同。select_related 执行了一个 sql join 查询。prefetch_related 执行一个单独的查找,它允许预先读取多对多和多对一的对象数据,这是 select_related 做不到的。另外 perfetch_related 也可以与通用外键和关系一起使用。

三. 何时使用 count、len、exists

有几种使用不同数据的情况,比如检查某个特定的数据是否存在,获取数据的数量或者获取完整的数据。有几种方法可以达到目的,但是我们需要在不同情况下选择最优的解决方式。
让我们看下下面的例子:

1. 判断数据是否存在
# Bad query
blogs = Blog.objects.filter(category='django')  
exists = len(blogs)>0

# Bad Query
exists = Blog.objects.filter(category='django').count() > 0

# Good query 最佳实践
exists = Blog.objects.filter(category='django').exists()
2. 获取数据的数量
# Bad query
blogs = Blog.objects.filter(category='django')  
count = len(blogs)

# Good Query 最佳实践
count = Blog.objects.filter(category='django').count()

当然,如果你也需要用到具体的数据的话,更好的方式就是先获取到数据,然后在后面需要计算数量的时候在使用 len 进行计算。

四. 直接使用外键值

如果你只想获取外键的 id, 通过 fk_id 的方法获取要优于 fk.id 的方式。fk.id 的方式会为子表内容保存额外的数据库查询。下面是代码示例:

# Bad query, additional db lookup in author table
blog = Blog.objects.get(id=2)
author_id = blog.author.id  

# A better version of above query but still a bad query
blog = Blog.objects.select_related('author').get(id=2)
author_id = blog.author.id  

# Good Query 最佳实践
blog = Blog.objects.get(id=2)
author_id = blog.author_id

五. 不要查询不需要的值

如果你明确知道需要使用的数据,那就没没必要查询数据库中所有列的数据。下面是几种实现方式。

1. 使用 values 和 values_list

如果一个表中的列非常多,而你只需要其中的一部分内容,可以通过 queryset.values() 与 queryset.values_list() 来获取指定列的字典或者列表,。values() 返回的是以字典的形式返回每一条数据,而 values_list() 返回的是每条数据元组的列表。

示例代码如下,获取分类为 django 的 Blog 的 title 和 author 的 name。通过 values() 和 values_list() 可以提高查询速度。

# Bad query
blogs = Blog.objects.filter(category='django').select_related('author')
my_list = []
for blog in blogs:
   mylist.append({'title':blog.title, 'author_name':blog.author.name}) 

# Good Query 最佳实践
mylist = Blog.objects.filter(category='django').values('title', 'author__name')
2. 使用 queryset.only 与 difer

也可以通过 queryset.only 与 difer 方法来优化查询性能。通过 only 方法可以只查询指定的列。而 difer 查询除了指定列之外的其他列。
代码示例如下:

# If you want only author and blog title 

# Bad query, this will retrieve all details
blogs = Blog.objects.filter(category='django').select_related('author')

# Good Query 最佳实践,只查询 Blog 的 title 与 author
blogs = Blog.objects.filter(category='django').only('author','title').select_related('author')

# If you want everything apart from description

# Bad query, this will retrieve all details
blogs = Blog.objects.filter(category='django').select_related('author')

# Good Query 最佳实践,查询除了 description 外 Blog 的其他字段值
blogs = Blog.objects.filter(category='django').defer('description').select_related('author')
3. 分析创建表索引

除了上面的优化技巧之外,一个更重要的事情就是要彻底理解你的数据。你应该深刻的了解你的数据结构和表的查询情况。了解之后就可以通过建立合适的索引来提高性能。记住,索引会降低写的性能,因此也要将这种情况考虑在内,最终得出合适的方案。

六. 安装 django-debug-tool 分析 Ajax 请求

django-debug-tool 是一个功能非常强大的 debug 插件,它可以展现项目中每次查询所执行的 SQL 语句的执行时间。这对我们在众多查询中找到比较耗时的查询非常有帮助。通过安装 django-debug-panal 插件或者其 chrome 插件也可以对 Ajax 请求进行分析。下面是其安装步骤:

pip install django-debug-toolbar
pip install django-debug-panel

# Add these to installed apps 
INSTALLED_APPS = (
    .....
    'debug_toolbar',
    'debug_panel',
    .....
)

# Add a middleware class
MIDDLEWARE_CLASSES += ('debug_panel.middleware.DebugPanelMiddleware',)

# Install this chrome extension
chrome extension(https://chrome.google.com/webstore/detail/django-debug-panel/nbiajhhibgfgkjegbnflpdccejocmbbn?hl=en)

在我所执行的查询中,共执行了 305 次查询,耗费了 23.2 秒

优化之前:
优化之前的性能

优化之后:
优化之后的性能

优化前后的性能差异非常巨大,执行时间从 26.2 降到了 3.8 秒,降低了 85.4%。造成性能的主要原因是在循环中进行了外键访问,导致了大量查询的执行。该实例是在一个很小的开发数据库中测试的。在生产中,执行时间从 3.6 秒降到了 3.65 秒,耗时降低了 82%。

记住,有时候执行一些额外查询或者联结进行复杂的查询可能会更快。但拿出时间来对它们进行性能的检验始终都是值得的。

以上就是关于 Django ORM 优化的一些技巧,希望对你有所帮助。

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值