============================ 数据库访问优化 ============================ Django的数据库层提供了很多方法来帮助开发者充分的利用他们的数据库。这篇文档上聚 合了相关的文档链接,同时附加了一些小技巧,当你打算优化你对数据库的使用时,可以 按照下面组织好的话题一步步来做。 性能第一 ============= 作为通用的编程实践,性能的重要性不言而喻。找出 `你执行了多少次查询以及它们花费了你多少时间 `。你或许想要通过一个外部的项目比如 django-debug-toolbar_, 或者一个工具可以直接的监控你的数据库。 记住,你可能会优化速度或内存甚至两者都优化,这取决于你的需求。有时对其中一项的优化会 不利于另一项,但有时也会相互得益。同样,让数据库来处理所有的工作和你用Python来处理 所花费的时间是不同的(对你来说)。哪一个优先级高些取决于你,你必须找到一个平衡点, 同时,衡量所有这些需求,因为这将依赖于你的应用程序和服务器。 按照下面的方法,切记对每一个改变做性能分析,以确保每次改变都是有益的,但是足够大的优 化可能会降低代码的可读性。**所有**下面这些建议都伴随着一个警告:在你所处的情况和原则 下,这些建议可能并不适用,甚至可能是有副作用的。 .. _django-debug-toolbar: https://github.com/django-debug-toolbar/django-debug-toolbar/ 使用标准的数据库优化技巧 ======================================= ...包括: * Indexes(索引)。这是首要的任务,在你分析完应该添加什么样的索引之后。使用 :attr:`django.db.models.Field.db_index` 从Django中添加索引。 * 使用适当的字段类型。 我们假设你已经完成了上面说的显而易见的优化。这篇文档剩下的部分将关注在如何用这样 的方式使用Django以避免不必要的工作。这篇文档并没有讨论其他的适用于花费时间较多操作的 优化技巧,比如 :doc:`通用缓存`。 理解QuerySets ==================== 理解 :doc:`QuerySets ` 是通过简单的代码获取较好性能至关重要 的一步。尤其是: 理解QuerySet的求值过程 ------------------------------ 为了避免性能上的问题,最好理解: * :ref:`QuerySets是惰性的 `. * 何时 :ref:`它们被执行 `. * 如何 :ref:`将数据保存在内存中 `. 理解被缓存的属性 ---------------------------- 除了缓存整个 ``QuerySet``之外,还会缓存ORM对象由属性获取到的结果。通常,不可被 调用的属性会被缓存。比如,假设这是 :ref:`Weblog models示例 `:: >>> entry = Entry.objects.get(id=1) >>> entry.blog # 此时的Blog对象会从数据库取出 >>> entry.blog # 这时的blog是缓存的版本,不会造成数据库访问 但是一般来说,可被调用的属性每次都会触发数据库查询:: >>> entry = Entry.objects.get(id=1) >>> entry.authors.all() # 执行查询 >>> entry.authors.all() # 再次执行查询 当阅读模板里的代码时应该注意 - 模板系统不允许使用括号,但它会自动调用可被调 用的属性,这隐藏了上面的差别。 小心的对待你自定义的属性 - 它需要由你来实现缓存。 使用 ``with`` 模板标签 ----------------------------- 为了使用 ``QuerySet`` 的缓存效果,你需要使用模板的 :ttag:`with` 标签。 使用 ``iterator()`` ------------------ 当你需要大量的对象时, ``QuerySet`` 的缓存行为会引起大量的内存使用。这种情况 下, :meth:`~django.db.models.query.QuerySet.iterator()` 可能是有帮助的。 让数据库来完成它自己的工作而不是用Python ====================================================== 比如: * 在最基本的层面上,使用 :ref:`filter and exclude `在数据库 中执行过滤操作。 * 使用 :ref:`F() object query expressions ` 在同一模型 中使用不同字段进行对比过滤。 * 使用 :doc:`注解来进行数据库的计算操作 `. 如果这些还不足以生成你需要的SQL的话,继续往下看: 使用 ``QuerySet.extra()`` ------------------------ 一个更轻便但是很强大的方法是 :meth:`~django.db.models.query.QuerySet.extra()`, 它允许一些SQL语句被显式 的添加到查询中。如果这仍然不够强大的话: 使用原生的SQL ----------- 自己来写 :doc:`自定义的SQL来获取数据或者添加你定义的models ` 。使用 ``django.db.connection.queries`` 来查看Django帮你写了什么SQL,从这里 入手。 如果你知道你会再次用到它,那就一次都获取过来 ======================================================== 多次访问数据库来分别获取一个你需要的数据集合的不同部分,通常来说,效率要低于 在一次请求中获取所有数据。这是很重要的,如果你在一个循环里执行查询,而且可能 会因此导致很多的数据库查询,当只需要一次查询的时候。因此: 使用 ``QuerySet.select_related()`` 和 ``prefetch_related()`` ------------------------------------------------------------ 彻底理解 :meth:`~django.db.models.query.QuerySet.select_related` 和 :meth:`~django.db.models.query.QuerySet.prefetch_related` , 然后使用它们: * 在查看代码时, * 以及在 :doc:`managers and default managers ` 中适当 的情况下。要意识到你的manager何时会用;有时这会很棘手,因此别妄自假设。 不要获取你不需要的数据 ==================================== 使用 ``QuerySet.values()`` 和 ``values_list()`` ----------------------------------------------- 当你仅仅只想获取一个 ``dict`` 或者 ``list`` 的值,并且不需要使用ORM模型的 objects时,适当地使用 :meth:`~django.db.models.query.QuerySet.values()`。 这些对于把模型对象替换为模板代码很有用 - 只要你提供的dicts和你在模板中使用 的数据有相同的属性,你就不会出错 。 使用 ``QuerySet.defer()`` 和 ``only()`` --------------------------------------- 当你知道数据库中的一些字段是你不需要的(或大多数情况不需要),为了避免加载 这些数据,你可以使用 :meth:`~django.db.models.query.QuerySet.defer()` 和 :meth:`~django.db.models.query.QuerySet.only()` 。注意,如果你确实这么做了 ,ORM将会不得不生成一个单独查询来获取这些数据,如果使用不当的话,会导致性能 变差。 同时,要意识到当构建一个带有延迟字段的模型时,在Django内部会有一些(小额的)额外 的开销。在没有进行性能分析之前不要太积极的使用延迟字段,因为数据库必须从磁盘上 读取一些非文本、非字符的数据作为结果中的一行,即使最终你只用到了很少的字段。 当你要避免加载很多文本数据或者针对那些可能会需要大量处理才能转为Python对象的 字段时, ``defer()`` 和 ``only()`` 方法是很有用的。和之前一样,先分析性能,然后 优化。 使用 QuerySet.count() -------------------- ...如果你只是想要获取有多少数据,不要使用 ``len(queryset)`` 。 使用 QuerySet.exists() --------------------- ...如果你只是想要知道是否至少存在一个结果,不要使用 ``if querysets`` 。 但是: 不要过度使用 ``count()`` 和 ``exists()`` ------------------------------------------ 如果你需要从QuerySet中获取其他数据,那要先评估一下。 比如,假设有一个Email的model,有一个 ``body`` 的属性和一个多对多关系的User 属性,下面的模板代码是最优的: .. code-block:: html+django {% if display_inbox %} {% with emails=user.emails.all %} {% if emails %}

You have {{ emails|length }} email(s)

{% for email in emails %}

{{ email.body }}

{% endfor %} {% else %}

No messages today.

{% endif %} {% endwith %} {% endif %} 它是最优的是因为: 1. 因为QuerySet是惰性的,如果 'display_inbox' 是False的话,这不会产生数据库 查询。 #. 使用 :ttag:`with` 意味着我们会存储 ``user.emails.all`` 在一个变量中供后面 使用,这允许被缓存以便重用。 #. 这句 ``{% if emails %}`` 其实是调用 ``QuerySet.__nonzero__()`` ,这会导致 ``user.emails.all()`` 这个查询在数据库层面运行,并且至少第一行会被转换成 一个ORM对象。如果没有任何结果,这句话将会返回False,反之则为True。 #. ``{{ emails|length }}`` 的使用将调用 ``QuerySet.__len__()``, 填充剩余的 缓存不需要调用另外的数据库查询。 #. :ttag:`for` 循环的迭代已经缓存在缓存中了。 总之,这段代码将会执行一次或者零次数据库查询。唯一值得思考的最优的执行操作是 因为用了 :ttag:`with` 标签。使用 ``QuerySet.exists()`` 或者 ``QuerySet.count()`` 在任何环境中都会引起额外的查询。 使用 ``QuerySet.update()`` 和 ``delete()`` ------------------------------------------ 比起获取大量数据,对其中一些赋值,然后单独保存,最好使用批量的SQL更新语句, 通过 :ref:`QuerySet.update()` 。同样,尽可能使用 :ref:`批量的删除操作` 。 值得注意的是,无论何时,批量的更新操作都不会调用单个实例中的 ``save()`` 和 ``delete()`` 方法,这意味着任何你添加在这些方法上的自定义的行文都将不会被 执行,包括任何从正常数据库对象发送过来的驱动信号。 直接使用外键的值 ------------------------------- 如果你仅仅是需要外键的值,使用你所使用对象已经获取到的外键值,而不是获取整个关系 对象然后拿到它的主键。比如,这样做:: entry.blog_id 而不是:: entry.blog.id 批量插入 ============== 当创建对象时,尽可能的使用 :meth:`~django.db.models.query.QuerySet.bulk_create()` 方法来减少SQL查询的的 数量。比如:: 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") 注意,有很多关于这个方法的说明 :meth:`对这些方法的警告`,因此, 确认这些方法适用于你所处的情景. 这同样适用于 :class:`ManyToManyFields `, 因此,这么做:: my_band.members.add(me, my_friend) ...而不是这么做:: my_band.members.add(me) my_band.members.add(my_friend) ...这里 ``Bands`` 和 ``Artists`` 是多对多的关系。