数据库访问优化

Django 的数据库层提供了多种方法来帮助开发人员充分利用其数据库。本文档收集了相关文档的链接,并添加了各种提示,这些提示按多个标题组织,概述了尝试优化数据库使用时要采取的步骤。

首先进行分析

作为通用的编程实践,这无需多言。找出您正在执行哪些查询以及它们的成本。使用 QuerySet.explain() 了解您的数据库如何执行特定的 QuerySet。您可能还想使用像 django-debug-toolbar 这样的外部项目,或直接监控您的数据库的工具。

请记住,您可能需要根据自己的需求优化速度、内存或两者兼顾。有时优化其中一项会损害另一项,但有时它们会互相帮助。此外,数据库进程完成的工作可能与您的 Python 进程中完成相同工作量所产生的成本(对您而言)不同。您需要决定自己的优先级、平衡点在哪里,以及根据需要对所有这些进行分析,因为这将取决于您的应用程序和服务器。

在以下所有内容中,请记住在每次更改后进行分析,以确保更改是有益的,并且考虑到代码可读性下降,更改带来的益处足够大。所有以下建议都带有这样的警告:在您的情况下,一般原则可能不适用,甚至可能相反。

使用标准的数据库优化技术

…包括

  • 索引。这是首要任务,您通过分析确定应该添加哪些索引之后。使用 Meta.indexesField.db_index 从 Django 添加这些索引。考虑将索引添加到您经常使用 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() 提供有关数据库如何执行查询的详细信息,包括使用的索引和联接。这些详细信息可能有助于您找到可以更有效地重写的查询,或识别可以添加以提高性能的索引。

在数据库中而不是在 Python 中进行数据库操作

例如

如果这些不足以生成您需要的 SQL

使用 RawSQL

一种不太便携但更强大的方法是 RawSQL 表达式,它允许将一些 SQL 显式添加到查询中。如果这仍然不够强大

使用原始 SQL

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

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

在使用 uniquedb_index 的列时,使用 get() 检索单个对象有两个原因。首先,由于底层数据库索引,查询将更快。此外,如果多个对象匹配查找条件,查询可能会运行得慢得多;在列上设置唯一约束可以保证这种情况永远不会发生。

因此,使用 示例博客模型

>>> 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()

当您只需要一个 dictlist 的值,并且不需要 ORM 模型对象时,请适当使用 values()。这些方法可以用来替换模板代码中的模型对象 - 只要您提供的字典具有与模板中使用的属性相同的属性,就可以正常工作。

使用 QuerySet.defer()only()

如果有一些数据库列您知道不需要(或者在大多数情况下不需要),可以使用 defer()only() 来避免加载它们。请注意,如果您确实使用了它们,ORM 将不得不通过单独的查询来获取它们,如果使用不当,这将导致性能下降。

在没有进行性能分析的情况下,不要过度使用字段延迟,因为即使最终只使用少数列,数据库也需要从磁盘读取结果中单行的大部分非文本、非VARCHAR数据。当你可以避免加载大量文本数据或对于需要大量处理才能转换回 Python 的字段时,defer()only() 方法最有用。一如既往,先进行性能分析,然后再优化。

使用 QuerySet.contains(obj)

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

使用 QuerySet.count()

…如果你只想获取计数,而不是执行 len(queryset)

使用 QuerySet.exists()

…如果你只想找出是否存在至少一个结果,而不是 if queryset

但是

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

如果你需要从 QuerySet 中获取其他数据,请立即对其进行评估。

例如,假设一个 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. 由于 QuerySet 是惰性的,如果 display_group_membersFalse,则不会执行任何数据库查询。
  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()

与其检索大量对象,设置一些值,然后逐个保存它们,不如使用批量 SQL UPDATE 语句,即通过 QuerySet.update()。类似地,在可能的情况下进行 批量删除

但是请注意,这些批量更新方法无法调用单个实例的 save()delete() 方法,这意味着您为这些方法添加的任何自定义行为都不会执行,包括任何由正常的数据库对象 信号 驱动的行为。

直接使用外键值

如果您只需要一个外键值,请使用您已有的对象上的外键值,而不是获取整个相关对象并获取其主键。例如,执行

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_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 之间存在多对多关系。请注意,此方法有一些 注意事项,因此请确保它适合您的用例。

批量删除

ManyToManyFields 中移除对象时,请使用 remove() 来移除多个对象,以减少 SQL 查询次数。例如

my_band.members.remove(me, my_friend)

…优于

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

…其中 BandsArtists 之间存在多对多关系。

ManyToManyFields 中移除不同的对象对时,请使用 delete() 对包含多个 through 模型实例的 Q 表达式进行操作,以减少 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 之间存在多对多关系。

返回顶部