数据库访问优化
Django 的数据库层提供了各种方法来帮助开发者最大限度地利用数据库。本文档收集了相关文档的链接,并添加了各种提示,按照一些标题组织,概述了在尝试优化数据库使用时的步骤。
首先性能分析
作为一般的编程实践,这个不用多说。找出 你在做什么查询以及它们花费的代价。使用 QuerySet.explain() 来了解你的数据库是如何执行特定的 QuerySet
的。你可能还想使用一个外部项目,比如 django-debug-toolbar ,或者一个直接监控数据库的工具。
请记住,你可能会根据你的需求,对速度或内存或两者进行优化。有时为其中之一进行优化会损害另一个,但有时它们会相互帮助。另外,由数据库进程完成的工作可能与在 Python 进程中完成的相同数量的工作的成本并不相同(对你来说)。这取决于你的优先级是什么,平衡点在哪里,并根据需要对所有这些进行性能分析,因为这将取决于你的应用程序和服务器。
对于下面的所有内容,请记住在每次修改后都要进行性能分析,以确保修改有好处,而且是一个足够大的好处,因为你的代码的可读性降低了。以下所有 的建议都有一个警告,那就是在你自身情况下,一般的原则可能不适用,甚至可能会被反过来。
使用标准数据库优化技巧
……包括:
Indexes 。这是第一优先级,在你从性能分析中确定应该添加哪些索引 之后。这是第一优先级的。使用 Meta.indexes 或 Field.db_index 从 Django 添加这些索引。可以考虑使用
filter()
、exclude()
、order_by()
等方式为你经常查询的字段添加索引,因为索引可能有助于加快查询速度。请注意,确定最好的索引是一个复杂的数据库依赖性话题,将取决于你的特定应用。维护索引的开销可能会超过查询速度的任何收益。合理使用字段类型。
我们将假设你已经做了上面列出的事情。本文档的其余部分主要介绍如何使用 Django,使你不做不必要的工作。本文档也不涉及其他适用于所有昂贵操作的优化技术,比如 通用缓存。
理解 QuerySet
理解 QuerySets 是用简单代码获得高效率的关键。特别是在:
理解 QuerySet
的执行过程
要避免执行过程中的问题,一定要理解:
- QuertSets 是惰性的。
- 当 它们被计算时。
- 不过 数据保存在内存中。
理解缓存属性
除了整个 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 代码中
例子:
- 在最基本的层面上,使用 filter 和 exclude 在数据库中进行过滤。
- 使用 F 表达式 根据同一模型中的其他字段进行过滤。
- 利用 注解在数据库中执行聚合。
若其不足以生成你需要的 SQL:
使用 RawSQL
最简单直接的方法是 RawSQL 表达式,它允许一些 SQL 显式的添加到查询中。如果这还不够强大:
使用原生 SQL
编写你自己的 自定义 SQL 来检索数据或填充模型。使用 django.db.connection.query
找出 Django 为你写的东西,然后从那里开始。
使用唯一索引列来检索单个对象。
当使用 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
没有被索引,这将使得底层数据库获取变慢。
其次,查找不保证只返回一个对象。如果查询匹配多于一个对象,它将从数据库中检索并传递所有对象。如果数据库位于单独的服务器上,那这个损失将更复杂,网络开销和延迟也是一个因素。
如果你明确需要它,那么立即检索所有内容。
对于你需要的所有部分的单个数据集的不同部分,多次访问数据库比单次查询所有内容的效率低。如果有一个查找,它在循环中执行,这点就尤其重要,当只需要一个查询时,最终会执行许多数据库查询。因此:
使用 QuerySet.select_related()
和 prefetch_related()
深入理解 select_related() 和 prefetch_related() ,并使用它们:
- 在 管理器和默认管理器 中使用。请注意管理器何时被使用;有时这很棘手,所以不要做出假设。
- 在视图代码层或其他层中,可能在需要时使用 prefetch_related_objects() 。
不要检索你不需要的东西
使用 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.")
这是最佳的,因为:
- 由于查询集是惰性加载的,如果
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()
如果要设置一些值并单独保存它们,而不是检索对象,那么可以通过 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)
其中 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
是多对多关系。注意这里有一些注意事项( 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)
其中 Bands
和 Artists
有多对多关系。
当从 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)
其中 Pizza
和 Topping
有多对多关系。