数据库访问优化

Django 的数据库层提供了各种方法来帮助开发者最大限度地利用数据库。本文档收集了相关文档的链接,并添加了各种提示,按照一些标题组织,概述了在尝试优化数据库使用时的步骤。

首先性能分析

作为一般的编程实践,这个不用多说。找出 你在做什么查询以及它们花费的代价。使用 QuerySet.explain() 来了解你的数据库是如何执行特定的 QuerySet 的。你可能还想使用一个外部项目,比如 django-debug-toolbar ,或者一个直接监控数据库的工具。

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

对于下面的所有内容,请记住在每次修改后都要进行性能分析,以确保修改有好处,而且是一个足够大的好处,因为你的代码的可读性降低了。以下所有 的建议都有一个警告,那就是在你自身情况下,一般的原则可能不适用,甚至可能会被反过来。

使用标准数据库优化技巧

……包括:

  • Indexes 。这是第一优先级,在你从性能分析中确定应该添加哪些索引 之后。这是第一优先级的。使用 Meta.indexesField.db_index 从 Django 添加这些索引。可以考虑使用 filter()exclude()order_by() 等方式为你经常查询的字段添加索引,因为索引可能有助于加快查询速度。请注意,确定最好的索引是一个复杂的数据库依赖性话题,将取决于你的特定应用。维护索引的开销可能会超过查询速度的任何收益。
  • 合理使用字段类型。

我们将假设你已经做了上面列出的事情。本文档的其余部分主要介绍如何使用 Django,使你不做不必要的工作。本文档也不涉及其他适用于所有昂贵操作的优化技术,比如 通用缓存

理解 QuerySet

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

理解 QuerySet 的执行过程

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

理解缓存属性

As well as caching of the whole QuerySet, there is caching of the result of attributes on ORM objects. In general, attributes that are not callable will be cached. For example, assuming the example blog models:

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

But in general, callable attributes cause DB lookups every time:

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

阅读模板代码时要注意——模板系统不允许使用括号,但会自动调用可调用对象代码,隐藏了上述区别。

小心使用你自己的自定义属性——在需要的时候由你自己来实现缓存,例如使用 cached_property 装饰器。

使用 with 模板标签

要使用 QuerySet 的缓存行为,你可能需要使用 with 模板标签。

使用 iterator()

当你有很多对象时,QuerySet 的缓存行为可能会导致大量的内存被使用。在这种情况下,iterator() 可能会有帮助。

使用 explain()

QuerySet.explain() 为你提供有关数据库如何执行查询的详细信息,包括使用的索引和连接(jion)。这些细节可能会帮助你找到可以更有效地重写的查询,或确定可以添加的索引以提高性能。

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

例子:

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

使用 RawSQL

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

使用原生 SQL

编写你自己的 自定义 SQL 来检索数据或填充模型。使用 django.db.connection.query 找出 Django 为你写的东西,然后从那里开始。

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

当使用 unique()db_index 的列来检索单个对象时,有两个原因。首先,由于底层数据库索引的存在,查询的速度会更快。另外,如果多个对象与查找对象相匹配,查询的运行速度可能会慢很多;在列上有一个唯一约束保证这种情况永远不会发生。

So using the example blog 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.

Use QuerySet.contains(obj)

...if you only want to find out if obj is in the queryset, rather than if obj in queryset.

使用 QuerySet.count()

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

使用 QuerySet.exists()

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

但是:

Don't overuse contains(), count(), and exists()

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

For example, assuming a Group model that has a many-to-many relation to User, the following code is optimal:

members = group.members.all()

if display_group_members:
    if members:
        if current_user in members:
            print("You and", len(members) - 1, "other users are members of this group.")
        else:
            print("There are", len(members), "members in this group.")

        for member in members:
            print(member.username)
    else:
        print("There are no members in this group.")

这是最佳的,因为:

  1. Since QuerySets are lazy, this does no database queries if display_group_members is False.
  2. Storing group.members.all() in the members variable allows its result cache to be reused.
  3. The line if members: causes QuerySet.__bool__() to be called, which causes the group.members.all() query to be run on the database. If there aren't any results, it will return False, otherwise True.
  4. The line if current_user in members: checks if the user is in the result cache, so no additional database queries are issued.
  5. The use of len(members) calls QuerySet.__len__(), reusing the result cache, so again, no database queries are issued.
  6. The for member loop iterates over the result cache.

In total, this code does either one or zero database queries. The only deliberate optimization performed is using the members variable. Using QuerySet.exists() for the if, QuerySet.contains() for the in, or QuerySet.count() for the count would each cause additional queries.

使用 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 ),因此要确保它适用于你的情况。

批量更新

当更新对象时,尽可能使用 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[0].save()
entries[1].headline = "This is no longer a test"
entries[1].save()

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

批量插入

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

my_band.members.add(me, my_friend)

要优于:

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

其中 BandsArtists 有多对多关系。

当不同的对象对插入到 ManyToManyField 或者自定义的 through 表被定义时,可以使用 bulk_create() 方法来减少 SQL 查询的数量。比如:

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)

...其中 PizzaTopping 是多对多关系。注意这里有一些注意事项( caveats to this method ),因此要确保它适用于你的案例。

批量删除

当从 ManyToManyFields 删除对象时,可以使用带有多个对象的 remove() 来减少 SQL 查询的数量。比如:

my_band.members.remove(me, my_friend)

要优于:

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

其中 BandsArtists 有多对多关系。

当从 ManyToManyFields 里删除不同的对象对时,可以在带有多种 through 模型实例的 Q 表达式上使用 delete() 来减少 SQL 查询的数量。比如:

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)

其中 PizzaTopping 有多对多关系。