django官方文档——数据库访问优化

时间:2022-09-07 06:02:20

数据库访问优化

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()   # 再次执行查询

在阅读模板代码时要小心,因为模板代码是不允许使用圆括号的,但仍会自动调用可调用的属性。

还要小心你自定义的属性,因为自定义属性是否调用缓存是你自己决定的。

使用 with 模板标记

要使用 查询集 的缓存行为,你可能需要使用 with 模板标记。

使用 iterator()

当你有大量对象时, 查询集 缓存行为可能消耗大量内存,在这种情况下使用 iterator() 可能有帮助。

不要在 Python 执行数据库操作,尽量在数据库中实现

例如:

如果上述内容还不足生成你所需要的 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) 好。

使用 QuerySet.exists()

...如果你只想要发现是否起码存在一个结果,那么 QuerySet.exists() 比 if queryset 好。

但是:

不要过度使用 count() exists()

如果你还需要查询集中的其他数据,那么就不要使用 count()exists()

例如,假设有一个电子邮件模型,这个模型有一个 body 属性并且有一个对应 User的多对多关系。下面的模板代码是最优的:

{% if display_inbox %}
  {% with emails=user.emails.all %}
    {% if emails %}
      <p>You have {{ emails|length }} email(s)</p>
      {% for email in emails %}
        <p>{{ email.body }}</p>
      {% endfor %}
    {% else %}
      <p>No messages today.</p>
    {% endif %}
  {% endwith %}
{% endif %}

以上代码最优的理由如下:

  1. 因为查询集是惰性的,所以如果 'display_inbox' 为假,那么就不会执行数据库查询。
  2. 使用 with 就意味我们把 user.emails.all 储存到变量中以备后用,也就是允许缓存它用于以后反复使用。
  3. {% if emails %} 这行代码引发 QuerySet.__nonzero__() 被调用,引发 user.emails.all() 查询在数据库中执行,并且至少使第一行转化为一个 ORM 对象。如果有结果则返回 True ,否则返回 False 。
  4. 使用 {{ emails|length }} 就是调用 QuerySet.__len__() ,这里调用缓存而不会再做查询。
  5. for 循环反复作用于缓存上。

总而言之,以上代码只会做一次或不做查询。代码中最重要的优化就是使用 with 标记。如果在任何地方使用 QuerySet.exists() QuerySet.count() 就会带来额外的查询。

使用 QuerySet.update()delete()

如果有大量对象要赋值并保存,那么与其一个一个分别执行,不如使用一个批量 SQL UPDATE 语句 QuerySet.update() 。同理,只要有可能就应当使用 批量删除

注意,这些批量操作语句不能调用每个实例的 save()delete() 方法,这就意味着你加在这些方法上的自定义行为就不会被执行,包括由一般数据库对象 信号 驱动的任何东西也不会被执行。

直接使用外键的值

如果你只需要一个外键的值,那么请直接使用你已获得的对象上的外键值,而不要获取整个关系对象再获得其主键。例如应当这样:

entry.blog_id

不应当这样:

entry.blog.id