数据库连接优化

Django 的数据库层提供了很多方式,帮助开发者充分使用它们的数据库。本文收集连接至关联文档,并添加了很多技巧,这些技巧被组织在多个标题下,这些标题概述了优化数据库时要采取的步骤。

先介绍一下

作为一般的编程实践,这是不言而喻的。找出:ref:what queries you are doing and what they are costing you <faq-see-raw-sql-queries>。用法:meth:.Queryset.explain`了解数据库如何执行特定的``Queryset`s。您可能还希望使用外部项目,如django-debug-toolbar,或直接监视数据库的工具。

请记住,根据您的需求,您可能正在优化速度或内存,或者两者兼而有之。有时,为一个优化会对另一个不利,但有时它们会互相帮助。另外,由数据库进程完成的工作可能与在Python进程中完成的工作量不同(对您来说)。这取决于您决定您的优先级是什么,平衡在哪里,并根据需要对所有这些进行分析,因为这将依赖于您的应用程序和服务器。

对于接下来的所有内容,请记住在每次更改之后进行概要分析,以确保更改是一个好处,并且考虑到代码可读性的降低,这是一个足够大的好处。以下**所有**建议都附带警告,即在您的情况下,一般原则可能不适用,甚至可能被推翻。

使用标准 DB 优化技巧

……包括:

  • Indexes。 这是第一优先级,在您通过分析应该添加哪些索引*之后*确定。 使用:attr:Meta.indexes <django.db.models.Options.indexes>`或:attr:`Field.db_index <django.db.models.Field.db_index>`从Django添加这些。 考虑将索引添加到您经常使用的字段:meth:`~django.db.models.query.QuerySet.filter(),:meth:~django.db.models.query.QuerySet.exclude(), :meth:`~django.db.models.query.QuerySet.order_by()`等作为索引可能有助于加快查找速度。 请注意,确定最佳索引是一个复杂的数据库相关主题,这依赖于您的特定应用程序。 维护索引的开销可能超过查询速度的任何增益。
  • 合理使用字段类型。

我们假设您已完成上述明显的事情。 本文档的其余部分重点介绍如何以不使用不必要的工作的方式使用Django。 本文档也没有涉及适用于所有昂贵操作的其他优化技术,例如:doc:general purpose caching</topics/cache>

理解 QuerySet

理解 QuerySets 是用简单代码获得高效率的关键。特别是在:

理解 QuerySet 的执行过程

要避免执行过程中的问题,一定要理解:

理解缓存属性

除了缓存整个``QuerySet``之外,还有ORM对象上属性结果的缓存。 通常,将缓存不可调用的属性。 例如,假设:ref:example Weblog models<queryset-model-example>

>>> entry = Entry.objects.get(id=1)
>>> entry.blog   # Blog object is retrieved at this point
>>> entry.blog   # cached version, no DB access

但一般来说,callable 属性每次都会触发 DB 查询:

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()   # query performed
>>> entry.authors.all()   # query performed again

阅读模板代码时要小心 - 模板系统不允许使用括号,但会自动调用callables,隐藏上述区别。

小心你自己的自定义属性 - 你需要在需要时实现缓存,例如使用:class:`~django.utils.functional.cached_property`装饰器。

使用 with 模板标签

要使用``QuerySet``的缓存行为,您可能需要使用:ttag:`with`模板标记。

使用 iterator()

当你有很多对象时,``QuerySet``的缓存行为会导致使用大量内存。 在这种情况下,:meth:`~django.db.models.query.QuerySet.iterator()`可能有所帮助。

使用 explain()

QuerySet.explain() 会有关于数据库如何执行查询的详细介绍,包括索引和联合(joins)的使用。这些细节可以帮助你如何更高效的重写查询,或者识别可以添加的所以来改进性能。

在数据库中执行数据库操作,而不是在 Python 代码中

例子:

若其不足以生成你需要的 SQL:

使用 RawSQL

最简单直接的方法是 RawSQL 表达式,它允许一些 SQL 显式的添加到查询中。如果这还不够强大:

使用原生 SQL

编写自定义的SQL来检索数据或填充模型。使用 django.db.connection.queries 来找出 Django 为你编写的内容并从那里开始。

使用唯一索引列来检索单个对象。

当使用 get() 来检索单一对象时,使用 uniquedb_index 的列有两个原因。首先,由于底层数据库索引,所以查询会更快。此外,如果多个对象与查找匹配,查询就会很慢。对列具有唯一约束保证不会发生这种情况。

因此使用 example Weblog models:

>>> entry = Entry.objects.get(id=10)

会比以下更快:

>>> entry = Entry.objects.get(headline="News Item Title")

因为 id 通过数据库索引,并且保证是唯一的。

执行以下操作可能非常慢:

>>> entry = Entry.objects.get(headline__startswith="News")

首先,headline 没有被索引,这将使得底层数据库获取变慢。

其次,查找不保证只返回一个对象。如果查询匹配多于一个对象,它将从数据库中检索并传递所有对象。如果数据库位于单独的服务器上,那这个损失将更复杂,网络开销和延迟也是一个因素。

如果你明确需要它,那么立即检索所有内容。

对于你需要的所有部分的单个数据集的不同部分,多次访问数据库比单次查询所有内容的效率低。如果有一个查找,它在循环中执行,这点就尤其重要,当只需要一个查询时,最终会执行许多数据库查询。因此:

不要检索你不需要的东西

使用 QuerySet.values()values_list()

当你只想得到字典或列表的值,并且不需要 ORM 模型对象时,可以适当使用 values() 。这些对于替换模板代码中的模型对象非常有用——只要你提供的字典具有与模板中使用时相同的属性就行。

使用 QuerySet.defer()only()

如果你明确不需要这个数据库列(或在大部分情况里不需要),使用 defer()only() 来避免加载它们。注意如果你使用它们,ORM 将必须在单独的查询中获取它们,如果你不恰当的使用,会让事情变得糟糕。

Don’t be too aggressive in deferring fields without profiling as the database has to read most of the non-text, non-VARCHAR data from the disk for a single row in the results, even if it ends up only using a few columns. The defer() and only() methods are most useful when you can avoid loading a lot of text data or for fields that might take a lot of processing to convert back to Python. As always, profile first, then optimize.

使用 QuerySet.count()

……如果你只想计数,不要使用 len(queryset)

使用 QuerySet.exists()

……若你只想要确认是否有至少存在一项满足条件的结果,而不是 if queryset

但是:

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

如果你需要查询集中的其他数据,请对其进行评估。

比如,假设 Email 模型有 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’ 是 False,就不会有数据库查询。
  2. 使用 with 意味着我们在一个变量中存储 user.emails.all 以供日后使用,允许重用缓存。
  3. {% if emails %} 会调用 QuerySet.__bool__() ,这会使得在数据库上执行 user.emails.all() 。如果没有结果,将返回 False,否则返回 True。
  4. {{ emails|length }} 会调用 QuerySet.__len__() ,填写剩余的缓存而不执行其他查询。
  5. for 循环遍历已有的缓存。

总之,这个代码会执行一条或零条数据库查询。唯一经过深思熟虑的优化是 with 标签。在任何时候使用 QuerySet.exists()QuerySet.count() 会导致额外的查询。

使用 QuerySet.update()delete()

如果要设置一些值并单独保存它们,而不是检索对象,那么可以通过 QuerySet.update() 使用批量 SQL UPDATE 语句。类似地,尽可能使用批量删除( bulk deletes )。

注意,尽管这些批量更新方法不会调用单独实例的 save()delete() 方法,这意味着你为这些方法添加的任何自定义行为都不会执行,包括来自正常数据库对象信号( signals )的任何内容。

直接使用外键值

如果只需要外键值,那么使用已有对象上的外键值,而不是获取所有相关对象并获取它的主键。比如:

entry.blog_id

替换成:

entry.blog.id

如无需要,不要排序结果

排序是耗时的;对每个字段的排序是数据库必须执行的操作。如果模型有一个默认排序( Meta.ordering )并且不需要它,那么可以通过调用没有参数的 order_by() 在查询集上删除它。

添加索引到你的数据库上可以帮助改进排序性能。

使用批量方法

使用批量方法来减少 SQL 语句数量。

批量创建

当创建对象时,尽可能使用 bulk_create() 方法来减少 SQL 查询数量。比如:

Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

要优于:

Entry.objects.create(headline='This is a test')
Entry.objects.create(headline='This is only a test')

注意这个方法有一些注意事项( caveats to this method ),因此要确保它适用于你的情况。

批量更新

New in Django 2.2:

当更新对象时,尽可能使用 bulk_update() 方法来减少 SQL 查询数。给定对象的列表或查询集:

entries = Entry.objects.bulk_create([
    Entry(headline='This is a test'),
    Entry(headline='This is only a test'),
])

下面示例:

entries[0].headline = 'This is not a test'
entries[1].headline = 'This is no longer a test'
Entry.objects.bulk_update(entries, ['headline'])

要优于:

entries[0].headline = 'This is not a test'
entries.save()
entries[1].headline = 'This is no longer a test'
entries.save()

注意此方法有一些 注意事项 ,因此确保它适合你的案例。

批量插入

当插入对象到 ManyToManyFields 时,使用带有多个对象的 add() 来减少 SQL 查询的数量。举例:

my_band.members.add(me, my_friend)

要优于:

my_band.members.add(me)
my_band.members.add(my_friend)

其中 BandsArtists 有多对多关系。

When inserting different pairs of objects into ManyToManyField or when the custom through table is defined, use bulk_create() method to reduce the number of SQL queries. For example:

PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create([
    PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
    PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
    PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
], ignore_conflicts=True)

要优于:

my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)

…where Pizza and Topping have a many-to-many relationship. Note that there are a number of caveats to this method, so make sure it’s appropriate for your use case.

Remove in bulk

When removing objects from ManyToManyFields, use remove() with multiple objects to reduce the number of SQL queries. For example:

my_band.members.remove(me, my_friend)

要优于:

my_band.members.remove(me)
my_band.members.remove(my_friend)

其中 BandsArtists 有多对多关系。

When removing different pairs of objects from ManyToManyFields, use delete() on a Q expression with multiple through model instances to reduce the number of SQL queries. For example:

from django.db.models import Q
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
    Q(pizza=my_pizza, topping=pepperoni) |
    Q(pizza=your_pizza, topping=pepperoni) |
    Q(pizza=your_pizza, topping=mushroom)
).delete()

要优于:

my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)

…where Pizza and Topping have a many-to-many relationship.