优化Django数据库访问
Django的ORM将程序员从编写繁杂的SQL语句中解放了出来, 但这并不代表程序员就不需要关心数据库的性能问题, 以下整理了一些关于在Django中优化数据库访问的建议。
Django 官方文档:Database access optimization
做性能测试
作为一个通用的编程实践,做性能测试意义是不言而明的。
- 找到开销最大的查询:https://docs.djangoproject.com/en/1.8/faq/models/#faq-see-raw-sql-queries
- 你也可以用django-debug-toolbar之类的工具
- 或者用工具直接监控数据库
记住你可能需要为了速度或者空间,或者同时为了两者而优化。有的时候优化一方会降低另一方的表现, 而有的时候两者会互相促进。你需要根据自己的实际需求做出选择和平衡。
对于以下的所有方法,记得在每次做出改变后重新运行性能测试,来确定这些改变确实是有益的, 而且获得的性能提高的收益大于代码可读性下降带来的损失。
使用标准的数据库优化技巧
例如:
- 索引。在性能测试之后,你能够确定需要添加哪些索引。添加合理的索引对于数据库的性能提升是十分显著的。
在Django中你可以通过
Field.db_index
或者Meta.index_together
来添加索引。 考虑给需要经常使用filter()
,exclude()
,order_by()
等等操作的列添加索引。索引可能有益于提升查询速度。 需要注意的是,什么样的索引才是最好的是一个和每个数据库具体现实相关的复杂的话题。 维护数据库索引的开销可能比使用索引带来的性能提升还要高,因此谨慎使用索引,不要滥用。 - 正确地使用django model的field type
理解QuerySets
充分理解Querysets对于使用简洁的代码获得高性能是至关重要的,例如:
理解QuerySet的求值过程
为了避免性能问题,理解以下几点非常重要:
- that QuerySets are lazy. QuerySets的懒加载机制
- when they are evaluated. QuerySet何时求值
- how the data is held in memory. 在内存中QuerySet的数据是如何存储的
理解属性的缓存机制
如同QuerySet整体有缓存机制一样,ORM对象的属性也有缓存。总体上来说,callable的属性不会被缓存。例如:
entry = Entry.objects.get(id=1)
entry.blog # 此时,从数据库获取了Blog对象
entry.blog # 缓存的版本,没有数据库访问
entry.authors.all() # 发生了数据库查询
entry.authors.all() # 再次发生了数据库查询
在使用django template的时候要十分小心,template系统不允许有括号,但是仍会调用callables
留意你的自定义properties, 如果需要的话,你需要自己实现缓存机制,例如使用
cached_property
装饰器
使用with
template标签
为了使用QuerySet的缓存机制,你需要使用with
template标签
使用iterator()
当你有很多对象时,QuerySet的缓存特性有可能带来大量的内存开销.在这种情况下,使用iterator()
可能有所帮助.
在数据库中做数据库工作,而不是在Python中
例如:
- 使用filter和exlude在数据库中做filtering
- 使用
F expressions
基于在同一个model里的其他fields做filter - User annotate to do aggregation in the database.
如果以上几点不足以生成你需要的SQL语句:
使用RawSQL
RawSQL
表达式可移植性较差,但是更加强大,它可以直接将一些SQL语句加入你的查询语句.
如果这还是不够强大:
使用原生SQL语句
Write your own custom SQL to retrieve data or populate models.
使用django.db.connection.queries
来找到django生成的SQL语句是什么样的,并在此基础上编写自己的版本.
使用唯一的,加过索引的列来检索单个对象
通过带有unique
或者db_index
的列来用get()
检索单个对象有两个好处.第一,得益于底层的数据库索引,检索速度会更快.
第二,如果返回多个检索结果的话,查询会变慢.在列上面添加unique约束可以确保这种情况不会发生.
例如:
entry = Entry.objects.get(id=10)
比这行更快:
entry = Entry.objects.get(headlinie="News Item Title")
因为id
有数据库索引并且保证是唯一的.
这一行有可能非常慢:
entry = Entry.objects.get(headline__startswith="News")
如果你知道你会需要它们,一次性获取所有的数据
Use QuerySet.select_related()
and prefetch_related()
不要获取你不需要的数据
使用 QuerySet.values()
和 values_list()
使用 QuerySet.defer()
和 only()
使用 QuerySet.count()
使用 QuerySet.exists()
不要滥用 count()
和 exists()
使用 QuerySet.update()
和 delete()
直接使用外健的值
如果你需要一个外健的值,直接使用已经在对象里的外健值,而不是获取整个关联的对象然后拿到它的主键,例如:
entry.blog_id
而不是:
entry.blog.id
如果你不关系结果的顺序,不要给他们排序
排序不是没有开销的,每一个需要排序的字段都会引发相应的数据库操作.如果一个model有默认排序(Meta.ordering
)
但是你并不需要它,通过在QuerySet
上调用不带参数的order_by()
来移除它.
批量插入
在创建对象时,如果可能的话,使用bulk_create()
方法来减少数据库查询的次数,例如:
Entry.objects.bulk_create([
Entry(headline="Python 3.0 Released"),
Entry(headline="Python 3.1 Planned")
])
比下面的方法更好:
Entry.objects.create(headline="Python 3.0 Released")
Entry.objects.create(headline="Python 3.1 Planned")
该方法有几个点需要注意:caveats to this method
对于ManyToManyFields
也可以批量插入, 例如:
my_band.members.add(me, my_friend)
比下面的方法更好:
my_band.members.add(me)
my_band.members.add(my_friend)