数据库访问优化

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

首先性能分析

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

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

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

使用标准数据库优化技巧

……包括:

  • 索引应当优先考虑,但要先通过性能分析确定应当添加哪些索引。在 Django 中,可使用 Meta.indexes 或 Field.db_index 添加索引。考虑为经常通过 filter()、exclude()、order_by() 等方法查询的字段建立索引,以加快查找速度。不过,确定最佳索引是一项与具体数据库及应用有关的复杂工作;维护索引的开销可能超过查询提速带来的收益。

  • 合理使用字段类型。

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

理解 QuerySet

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

理解 QuerySet 的执行过程

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

理解缓存属性

除了整个 QuerySet 的缓存之外,还对 ORM 对象上的属性结果进行了缓存。通常,不可调用的属性会被缓存。例如,假设 示例博客模型:

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

但一般来说,可调用属性每次都会导致数据库查询:

>>> 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 为你写的东西,然后从那里开始。

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

在使用 get() 来检索单个对象时,有两条理由可以使用带有属性 unique 或 db_index 的列。首先,查询将更快,因为底层数据库索引的存在加快了速度。此外,如果多个对象匹配查找条件,则查询可能会运行得非常慢;在该列上设置唯一约束可以保证这种情况永远不会发生。

因此,使用 示例博客模型:

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

会比以下更快:

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

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

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

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

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

其次,这种查找可能返回多个结果。即使 Django 在使用 get() 时限制最多获取 21 个对象,数据库仍可能需要扫描全部数据才能判断一个非唯一条件。在查询开始时就缩小范围会更高效。

不要检索你不需要的东西

使用 QuerySet.values() 和 values_list()

如果只需要一个字典或值列表,不需要 ORM 模型对象,请适当地使用 values()。它可用于替代模板代码中的模型对象;只要提供的字典具有模板中使用的同名属性即可。

使用 QuerySet.defer() 和 only()

如果确定某些数据库列不需要使用,或大多数情况下不需要使用,可通过 defer() 和 only() 避免加载它们。不过,如果随后访问这些列,ORM 就必须另发查询来获取数据;因此,不恰当的使用反而会降低性能。

在不进行性能分析的情况下,不要过于激进地推迟字段,因为数据库必须从磁盘读取大多数非文本、非 VARCHAR 数据,即使最终只使用了几列。defer() 和 only() 方法在你可以避免加载大量文本数据或对需要大量处理才能转换回 Python 的字段时最有用。与往常一样,首先进行性能分析,然后进行优化。

使用 QuerySet.contains(obj)

…如果你只想找出 obj 是否在查询集中,而不是 if obj in queryset。

使用 QuerySet.count()

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

使用 QuerySet.exists()

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

但是:

不要滥用 contains(), count(), 和 exists()。

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

例如,假设有一个 Group 模型,它与 User 有一个多对多关系,下面的代码是最优的:

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. 由于查询集是惰性加载的,如果 display_group_members 是 False,则不会执行任何数据库查询。

  2. 将 group.members.all() 存储在 members 变量中可以重复使用其结果缓存。

  3. 这一行代码 if members: 导致调用了 QuerySet.__bool__(),这将导致在数据库上运行 group.members.all() 查询。如果没有结果,它将返回 False,否则返回 True。

  4. 这一行代码 if current_user in members: 检查用户是否在结果缓存中,因此不会发出额外的数据库查询。

  5. 使用 len(members) 调用了 QuerySet.__len__(),重复使用了结果缓存,因此再次不会发出数据库查询。

  6. for member 循环遍历结果缓存。

总的来说,这段代码只执行了一个或零个数据库查询。唯一明确的优化是使用了 members 变量。使用 QuerySet.exists() 来进行 if、使用 QuerySet.contains() 来进行 in,或者使用 QuerySet.count() 来进行计数都会导致额外的查询。

使用 QuerySet.update() 和 delete()

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

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

直接使用外键值

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

entry.blog_id

替换成:

entry.blog.id

如无需要,不要排序结果

排序并非没有成本;每个排序字段都需要数据库执行相应操作。如果模型定义了默认排序(Meta.ordering),但当前查询不需要排序,可在 QuerySet 上调用不带参数的 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")

bulk_create() 有若干注意事项,请先确认它适合当前使用场景。

批量更新

更新对象时,尽可能使用 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()

bulk_update() 有若干注意事项,请先确认它适合当前使用场景。

批量插入

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

my_band.members.add(me, my_friend)

要优于:

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

这里的 Band 和 Artist 是具有多对多关系的模型。

向 ManyToManyField 插入多组不同的对象关系,或者定义了自定义中间表时,可使用 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)

…其中 Pizza 和 Topping 是多对多关系。注意这里有一些注意事项( 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)

这里的 Band 和 Artist 是具有多对多关系的模型。

如果需要移除涉及多个关联模型实例的多对多关系,可筛选该字段中间模型的查询集,然后调用 delete()。通过 Q() 对象组合多个条件,可以在一次查询中删除多条关系。例如:

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)

其中 Pizza 和 Topping 有多对多关系。


原文:数据库访问优化。作者/来源:Django 项目与文档贡献者。本文依据所列原文整理为中文,代码、命令与配置示例保留原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容