数据库访问优化¶
Django 的数据库层提供了多种方法来帮助开发人员充分利用其数据库。本文档收集了相关文档的链接,并添加了各种提示,这些提示按多个标题组织,概述了尝试优化数据库使用时要采取的步骤。
首先进行分析¶
作为通用的编程实践,这无需多言。找出您正在执行哪些查询以及它们的成本。使用 QuerySet.explain()
了解您的数据库如何执行特定的 QuerySet
。您可能还想使用像 django-debug-toolbar 这样的外部项目,或直接监控您的数据库的工具。
请记住,您可能需要根据自己的需求优化速度、内存或两者兼顾。有时优化其中一项会损害另一项,但有时它们会互相帮助。此外,数据库进程完成的工作可能与您的 Python 进程中完成相同工作量所产生的成本(对您而言)不同。您需要决定自己的优先级、平衡点在哪里,以及根据需要对所有这些进行分析,因为这将取决于您的应用程序和服务器。
在以下所有内容中,请记住在每次更改后进行分析,以确保更改是有益的,并且考虑到代码可读性下降,更改带来的益处足够大。所有以下建议都带有这样的警告:在您的情况下,一般原则可能不适用,甚至可能相反。
使用标准的数据库优化技术¶
…包括
- 索引。这是首要任务,在您通过分析确定应该添加哪些索引之后。使用
Meta.indexes
或Field.db_index
从 Django 添加这些索引。考虑将索引添加到您经常使用filter()
、exclude()
、order_by()
等查询的字段,因为索引可能有助于加快查找速度。请注意,确定最佳索引是一个复杂的数据库相关主题,将取决于您的特定应用程序。维护索引的开销可能会超过查询速度的任何提升。
- 适当使用字段类型。
我们将假设您已经完成了上述操作。本文档的其余部分重点介绍如何使用 Django,以避免进行不必要的工作。本文档也不涉及适用于所有昂贵操作的其他优化技术,例如 通用缓存。
了解 QuerySet
¶
理解 QuerySets 对用简单的代码获得良好的性能至关重要。特别是
理解缓存属性¶
除了缓存整个 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
装饰器。
使用 iterator()
¶
当你有大量对象时,QuerySet
的缓存行为会导致使用大量的内存。在这种情况下,iterator()
可能会有所帮助。
使用 explain()
¶
QuerySet.explain()
提供有关数据库如何执行查询的详细信息,包括使用的索引和联接。这些详细信息可能有助于您找到可以更有效地重写的查询,或识别可以添加以提高性能的索引。
在数据库中而不是在 Python 中进行数据库操作¶
例如
- 在最基本层面上,使用 filter 和 exclude 在数据库中进行过滤。
- 使用
F expressions
根据同一模型中的其他字段进行过滤。 - 使用 annotate 在数据库中进行聚合.
如果这些不足以生成您需要的 SQL
使用原始 SQL¶
编写您自己的 自定义 SQL 来检索数据或填充模型。使用 django.db.connection.queries
找出 Django 为您编写的内容,并从那里开始。
使用唯一索引列检索单个对象¶
在使用 unique
或 db_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()
¶
当您只需要一个 dict
或 list
的值,并且不需要 ORM 模型对象时,请适当使用 values()
。这些方法可以用来替换模板代码中的模型对象 - 只要您提供的字典具有与模板中使用的属性相同的属性,就可以正常工作。
使用 QuerySet.defer()
和 only()
¶
如果有一些数据库列您知道不需要(或者在大多数情况下不需要),可以使用 defer()
和 only()
来避免加载它们。请注意,如果您确实使用了它们,ORM 将不得不通过单独的查询来获取它们,如果使用不当,这将导致性能下降。
在没有进行性能分析的情况下,不要过度使用字段延迟,因为即使最终只使用少数列,数据库也需要从磁盘读取结果中单行的大部分非文本、非VARCHAR
数据。当你可以避免加载大量文本数据或对于需要大量处理才能转换回 Python 的字段时,defer()
和 only()
方法最有用。一如既往,先进行性能分析,然后再优化。
使用 QuerySet.contains(obj)
¶
…如果你只想找出 obj
是否在查询集中,而不是 if obj in queryset
。
使用 QuerySet.count()
¶
…如果你只想获取计数,而不是执行 len(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.")
它是最佳的,因为
- 由于 QuerySet 是惰性的,如果
display_group_members
为False
,则不会执行任何数据库查询。 - 将
group.members.all()
存储在members
变量中,可以重复使用其结果缓存。 - 代码行
if members:
会调用QuerySet.__bool__()
,进而导致group.members.all()
查询在数据库上执行。如果查询结果为空,则返回False
,否则返回True
。 - 代码行
if current_user in members:
检查用户是否在结果缓存中,因此不会发出额外的数据库查询。 - 使用
len(members)
会调用QuerySet.__len__()
,并重用结果缓存,因此同样不会发出数据库查询。 - 循环
for member
会遍历结果缓存。
总而言之,这段代码最多执行一次数据库查询,或者根本不执行。唯一刻意进行的优化是使用 members
变量。使用 QuerySet.exists()
用于 if
,使用 QuerySet.contains()
用于 in
,或者使用 QuerySet.count()
用于计数,都会导致额外的查询。
使用 QuerySet.update()
和 delete()
¶
与其检索大量对象,设置一些值,然后逐个保存它们,不如使用批量 SQL UPDATE 语句,即通过 QuerySet.update()。类似地,在可能的情况下进行 批量删除。
但是请注意,这些批量更新方法无法调用单个实例的 save()
或 delete()
方法,这意味着您为这些方法添加的任何自定义行为都不会执行,包括任何由正常的数据库对象 信号 驱动的行为。
如果您不关心,请不要对结果进行排序¶
排序并非免费;每个要排序的字段都是数据库必须执行的操作。如果模型具有默认排序(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)
…其中 Bands
和 Artists
之间存在多对多关系。
将不同的对象对插入 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)
…其中 Pizza
和 Topping
之间存在多对多关系。请注意,此方法有一些 注意事项
,因此请确保它适合您的用例。
批量删除¶
从 ManyToManyFields
中移除对象时,请使用 remove()
来移除多个对象,以减少 SQL 查询次数。例如
my_band.members.remove(me, my_friend)
…优于
my_band.members.remove(me)
my_band.members.remove(my_friend)
…其中 Bands
和 Artists
之间存在多对多关系。
从 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)
…其中 Pizza
和 Topping
之间存在多对多关系。