数据库访问优化 — Django 文档
数据库访问优化
Django 的数据库层提供了多种方法来帮助开发人员充分利用他们的数据库。 本文档收集了相关文档的链接,并添加了各种提示,按照多个标题进行组织,概述了尝试优化数据库使用时要采取的步骤。
个人资料第一
作为一般的编程实践,这是不言而喻的。 找出 您正在执行的查询以及它们的成本 。 使用 QuerySet.explain() 了解您的数据库如何执行特定的 QuerySet
。 您可能还想使用像 django-debug-toolbar 这样的外部项目,或者直接监视数据库的工具。
请记住,您可能正在优化速度或内存或两者,具体取决于您的要求。 有时为一个进行优化会对另一个不利,但有时它们会互相帮助。 此外,数据库进程完成的工作的成本(对您而言)可能与 Python 进程中完成的工作量不同。 由您决定优先级是什么,平衡点在哪里,并根据需要分析所有这些,因为这取决于您的应用程序和服务器。
对于接下来的所有内容,请记住在每次更改后进行概要分析,以确保更改是有益的,并且在代码可读性降低的情况下具有足够大的益处。 下面的所有建议都附带警告,在您的情况下,一般原则可能不适用,甚至可能被颠倒。
使用标准的数据库优化技术
…包含:
- 索引。 这是第一要务,after 您已经通过分析确定应该添加哪些索引。 使用 Meta.indexes 或 Field.db_index 从 Django 添加这些。 考虑为您经常使用 filter()、exclude()、order_by() 等查询的字段添加索引。 因为索引可能有助于加快查找速度。 请注意,确定最佳索引是一个复杂的数据库相关主题,这将取决于您的特定应用程序。 维护索引的开销可能超过查询速度的任何收益。
- 适当使用字段类型。
我们假设您已经完成了上面列出的事情。 本文档的其余部分重点介绍如何以不做不必要的工作的方式使用 Django。 本文档也不涉及适用于所有昂贵操作的其他优化技术,例如 通用缓存 。
了解 QuerySets
了解 QuerySets 对于使用简单代码获得良好性能至关重要。 特别是:
了解缓存属性
除了缓存整个 QuerySet
,还有缓存 ORM 对象上的属性结果。 通常,不可调用的属性将被缓存。 例如,假设 示例 Weblog 模型 :
>>> 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 装饰器。
使用 explain()
QuerySet.explain() 提供有关数据库如何执行查询的详细信息,包括使用的索引和连接。 这些详细信息可以帮助您找到可以更有效地重写的查询,或确定可以添加以提高性能的索引。
在数据库中而不是在 Python 中进行数据库工作
例如:
- 在最基本的层面上,使用 filter 和 exclude 在数据库中进行过滤。
- 使用 F 表达式 根据同一模型中的其他字段进行过滤。
- 使用annotate在数据库中做聚合。
如果这些不足以生成您需要的 SQL:
使用唯一的索引列检索单个对象
在使用 get() 检索单个对象时,使用带有 unique 或 db_index 的列有两个原因。 首先,由于底层数据库索引,查询会更快。 此外,如果多个对象与查找匹配,查询的运行速度可能会慢得多; 在列上有一个唯一的约束保证这永远不会发生。
因此,使用 示例 Weblog 模型 :
>>> 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() 以避免加载它们。 请注意,如果您 do 使用它们,则 ORM 将不得不在单独的查询中获取它们,如果您不恰当地使用它,这将导致悲观。
在没有分析的情况下推迟字段不要太激进,因为数据库必须从磁盘读取大多数非文本、非 VARCHAR 数据以获取结果中的一行,即使它最终只使用了几列。 defer()
和 only()
方法在您可以避免加载大量文本数据或可能需要大量处理才能转换回 Python 的字段时最有用。 一如既往,先配置文件,然后优化。
使用 QuerySet.count()
...如果你只想要计数,而不是做 len(queryset)
。
使用 QuerySet.exists()
...如果您只想找出是否存在至少一个结果,而不是 if queryset
。
但是:
不要过度使用 count() 和 exists()
如果您需要来自 QuerySet 的其他数据,请立即评估它。
例如,假设电子邮件模型具有 subject
属性并且与 User 具有多对多关系,以下代码是最佳的:
if display_emails:
emails = user.emails.all()
if emails:
print('You have', len(emails), 'emails:')
for email in emails:
print(email.subject)
else:
print('You do not have any emails.')
它是最佳的,因为:
- 由于 QuerySets 是惰性的,如果
display_emails
是False
,则不会进行数据库查询。 - 将
user.emails.all()
存储在emails
变量中可以重用其结果缓存。 - 行
if emails
导致调用QuerySet.__bool__()
,从而导致user.emails.all()
查询在数据库上运行。 如果没有任何结果,则返回False
,否则返回True
。 - 使用
len(emails)
调用QuerySet.__len__()
,重用结果缓存。 for
循环遍历已填充的缓存。
总的来说,此代码执行一次或零次数据库查询。 执行的唯一优化是使用 emails
变量。 将 QuerySet.exists()
用于 if
或 QuerySet.count()
用于计数都会导致额外的查询。
使用 QuerySet.update() 和 delete()
与其检索大量对象、设置一些值并单独保存它们,不如通过 QuerySet.update() 使用批量 SQL UPDATE 语句。 同样,尽可能执行 批量删除 。
但是请注意,这些批量更新方法无法调用单个实例的 save()
或 delete()
方法,这意味着您为这些方法添加的任何自定义行为都不会执行,包括任何驱动来自普通数据库对象 信号 。
直接使用外键值
如果您只需要一个外键值,请使用您已获得的对象上已有的外键值,而不是获取整个相关对象并获取其主键。 IE 做:
entry.blog_id
代替:
entry.blog.id
如果您不在乎,请不要订购结果
订购不是免费的; 要排序的每个字段都是数据库必须执行的操作。 如果模型具有默认排序 (Meta.ordering) 并且您不需要它,请通过调用 order_by() 不带参数在 QuerySet
上将其删除.
向数据库添加索引可能有助于提高排序性能。
使用批量方法
使用批量方法来减少 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中插入不同的对象对或定义自定义到表时,使用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 中删除不同的对象对时,在具有多个 到 模型实例的 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
具有多对多关系。