数据库访问优化¶
Django 的数据库层提供了多种方法来帮助开发者尽可能的远离数据库底层。本文档收集了多种文档的链接,并增加了许多技巧。当你尝试优化数据库使用时,可以把本文作为一个大纲作为参考。
评估第一¶
作为通常的编程实践,这是不言而喻的。要知道到 你到底在做什么查询,这些查询的成本是多少 。你可能还需要使用象 django-debug-toolbar 之类的其他项目或直接监视你的数据库的工具。
请记住,你可能为了速度或者内存或者两者兼而有之作优化,这视你的需要而定。当你为其中一个目标优化后可能会损害另一个目标,但有时也会两者都受惠。同时作数据库优化可能还不如优化 Python 代码。这些都取决于你的重点在哪里,平衡点在哪里。优化依赖于你的应用和服务器,因此我们必须进行评估。
在进行优化时应牢记在每次变动之后应当进行评估,以确保变动有益的,并且益处在远大于降低代码可读性带来的害处。下文 所有 建议都有一个附加警示:在你的环境中,一般的原则可能失效,甚至是有害的。
使用标准数据库技术¶
...包括:
- 建立索引。在你评估应当建立哪些索引 之后 ,建立索引第一件要做的事。可以使用 django.db.models.Field.db_index 来建立索引。
- 选择适当的字段类型。
本文我们假设你已经把上述显而易见的事情都做好了。下文主要关注在你做了必要的工作的情况下如何使用 Django 。同时本文也不涉及重量级的操作,如 一般目标下的缓存 。
理解查询集¶
理解 查询集 是非常重要的,只有理解了查询集才可能用简明的代码获得优异的性能。尤其要掌握以下几点:
理解缓存属性¶
和缓存整个 查询集 一样, ORM 对象的属性也是有缓存的。通常,不可调用的属性会被缓存。例如,假设 一个示例博客模型:
>>> entry = Entry.objects.get(id=1)
>>> entry.blog # 获得 Blog 对象
>>> entry.blog # 只调用缓存,不操作数据库
但是通常可调用的属性每次都会引发数据库查询:
>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all() # 执行查询
>>> entry.authors.all() # 再次执行查询
在阅读模板代码时要小心,因为模板代码是不允许使用圆括号的,但仍会自动调用可调用的属性。
还要小心你自定义的属性,因为自定义属性是否调用缓存是你自己决定的。
使用 iterator()¶
当你有大量对象时, 查询集 缓存行为可能消耗大量内存,在这种情况下使用 iterator() 可能有帮助。
不要在 Python 执行数据库操作,尽量在数据库中实现¶
例如:
- 在多数基本层面,通过使用 过滤和排除 来在数据库中过滤。
- 使用 F() 对象查询表达式 来过滤同一模型中的其他字段。
- 通过使用 统计 来在数据库中进行统计。
如果上述内容还不足生成你所需要的 SQL ,那么可以:
使用 QuerySet.extra()¶
一个不是很轻便但是更有力的方法是使用 extra() 。它可以允许在查询中显式地加入一些 SQL 。如果还不够,可以:
使用原始 SQL¶
你可以使用 自己写的 SQL 语句 来获取数据或生成模型。在使用原始 SQL 之前,请先使用 django.db.connection.queries 来查看 Django 是如何为你生成 SQL 的。
一次性获取你所要的一切¶
分批从数据库中获得你要的数据往往比一次性获得所有数据效率低。当在一个循环中这样获得数据时时效率更低,这会带查询数量的激增。所以请:
不要获取你不需要的东西¶
使用 QuerySet.values() 和 values_list()¶
当你只需要一个包含值的 dict 或 list ,且不需要 ORM 模型对象时,请正确使用 values() 。这个方法可用于在模板代码中代替模型对象,因为你提供的字典提供了同样的属性。.
使用 QuerySet.defer() 和 only()¶
如果数据库中有不需要(或者大多数情况下不需要)的列时请使用 defer() 和 only() ,这样可以载入这些列。注意,如果你 不 使用它们, ORM 将只能通过一个单独的查询去获取这些列,这样就不好了。
同时,应当看到当使用延时的字段构建模型时, Django 内部还是需要花费一定成本的。因此不要不经过评估就乱用延时字段,毕竟为了一行数据,哪怕最后只一小部分列,数据库还是得从磁盘读入大多数非文本或非字符类型的数据的。 defer() 和 only() 方法最适用于避免载入大量文本情形下,因为把大量文本转换为 Python 对象还是很消耗资源的。但是还是要唠叨一句:先评估,再优化。
使用 QuerySet.count()¶
...如果你只想要计数,那么 QuerySet.count() 比 len(queryset) 好。
不要过度使用 count() 和 exists()¶
如果你还需要查询集中的其他数据,那么就不要使用 count() 和 exists() 。
例如,假设有一个电子邮件模型,这个模型有一个 body 属性并且有一个对应 User的多对多关系。下面的模板代码是最优的:
以上代码最优的理由如下:
- 因为查询集是惰性的,所以如果 'display_inbox' 为假,那么就不会执行数据库查询。
- 使用 with 就意味我们把 user.emails.all 储存到变量中以备后用,也就是允许缓存它用于以后反复使用。
- {% if emails %} 这行代码引发 QuerySet.__nonzero__() 被调用,引发 user.emails.all() 查询在数据库中执行,并且至少使第一行转化为一个 ORM 对象。如果有结果则返回 True ,否则返回 False 。
- 使用 {{ emails|length }} 就是调用 QuerySet.__len__() ,这里调用缓存而不会再做查询。
- for 循环反复作用于缓存上。
总而言之,以上代码只会做一次或不做查询。代码中最重要的优化就是使用 with 标记。如果在任何地方使用 QuerySet.exists() 或 QuerySet.count() 就会带来额外的查询。
使用 QuerySet.update() 和 delete()¶
如果有大量对象要赋值并保存,那么与其一个一个分别执行,不如使用一个批量 SQL UPDATE 语句 QuerySet.update() 。同理,只要有可能就应当使用 批量删除 。
注意,这些批量操作语句不能调用每个实例的 save() 或 delete() 方法,这就意味着你加在这些方法上的自定义行为就不会被执行,包括由一般数据库对象 信号 驱动的任何东西也不会被执行。
直接使用外键的值¶
如果你只需要一个外键的值,那么请直接使用你已获得的对象上的外键值,而不要获取整个关系对象再获得其主键。例如应当这样:
entry.blog_id
不应当这样:
entry.blog.id