数据库访问优化

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 为你编写的内容,并以此为起点。

使用唯一的已编制索引的列检索单个对象

当使用get()检索单个对象时,有两个原因要使用具有uniquedb_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.values()values_list()

当您只需要dictlist值,并且不需要ORM模型对象时,请适当地使用values()。这些对于替换模板代码中的模型对象很有用——只要您提供的字典具有与模板中使用的属性相同的属性,就可以了。

使用QuerySet.defer()only()

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

不要在没有分析的情况下过度使用字段延迟,因为数据库必须读取结果中每一行的大部分非文本、非VARCHAR数据,即使最终只使用几列。当您可以避免加载大量文本数据或对于可能需要大量处理才能转换回 Python 的字段时,defer()only()方法最有用。与以往一样,先分析,再优化。

使用QuerySet.contains(obj)

……如果您只想确定obj是否在queryset中,而不是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变量。对于if使用QuerySet.exists(),对于in使用QuerySet.contains(),或者对于计数使用QuerySet.count(),都会导致额外的查询。

使用QuerySet.update()delete()

与其检索大量对象、设置一些值并单独保存它们,不如通过QuerySet.update()使用批量SQL UPDATE语句。同样,尽可能进行批量删除

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

直接使用外键值

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

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

注意,此方法存在一些caveats to this method,请确保它适合您的用例。

批量更新

更新对象时,尽可能使用bulk_update()方法来减少SQL查询次数。给定一个对象列表或queryset

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

注意,此方法存在一些caveats to this method,请确保它适合您的用例。

批量插入

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具有多对多关系。注意,此方法存在一些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)

……其中BandsArtists具有多对多关系。

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)

……其中PizzaTopping具有多对多关系。

返回顶部