发布于 2025-12-07 0 阅读
0

优化 Django ORM 使用 ORM,Luke Hasta la vista,模型 向我展示 SQL(第一部分) 向我展示 SQL(第二部分) 向我展示 SQL(第三部分) 一个工具栏控制它们全部 选择并预取所有相关项 小心模型的实例化 根据 ID 进行过滤让世界运转 只遵从你的内心内容 注释并继续 批量粉碎!呃,创建 我们想让你变得庞大 会让你出汗(现在每个人都使用 Raw Sql) 穿上丽兹

优化 Django ORM

使用 ORM,Luke

再见,模特们

展示 SQL(第一部分)

展示 SQL(第 2 部分)

展示 SQL(第 3 部分)

一个工具栏控制所有工具栏

选择并预取所有相关

注意模型的实例化

通过 ID 进行过滤让世界运转起来

只遵从你的心意

注释并继续

批量粉碎!呃,创建

我们想让增肌

会让你出汗(现在每个人都使用 Raw Sql)

尽享奢华

最近,我一直在优化一些比预期慢的函数。与大多数 MVP 一样,最初的迭代是为了使其能够正常工作并投入使用。查看Scout APM发现,一些数据库查询速度很慢,其中包括几个n+1查询。n+1出现这些查询的原因是,我循环遍历一组模型,并且在每个模型中更新或选择了相同的内容。我的目标是减少任何重复查询,并通过将简单、直接的操作重构为性能更高的等效操作来尽可能地提升性能。

老实说,现在代码读起来稍微复杂一些,但我将用例的时间缩短了一半,而没有改变服务器或数据库的任何其他内容。

使用 ORM,Luke

Django 的主要优势之一是其内置的模型和对象关系映射器 (ORM)。它为模型提供了快速易用、通用的数据操作接口,并且可以轻松处理大多数查询。一旦您理解了语法,它还可以处理一些复杂的 SQL 语句。

快速构建很容易,但最终执行的 SQL 调用也可能比你意识到的要多(而且代价高昂)。

再见,模特们

这里有一些示例模型,将用于说明下面的一些概念。

# models.py
class Author(models.Model):
    name = models.CharField(max_length=50)

class Book(models.Model):
    author = models.ForeignKey(Author, related_name="books", on_delete=models.PROTECT)
    title = models.CharField(max_length=255)
Enter fullscreen mode Exit fullscreen mode

展示 SQL(第一部分)

由于 SQL 调用抽象到一个简单的 API 后面,因此最终很容易产生比你意识到的更多的 SQL 调用。你可以使用 QuerySet 上的属性来检索近似值query,但要注意它是一种“不透明表示”。

books = Book.objects.all()
print("books.query", books.query)
Enter fullscreen mode Exit fullscreen mode

展示 SQL(第 2 部分)

您还可以将其添加django.db.logging到已配置的记录器中,以查看生成的 SQL 是否打印到控制台。

"loggers": {
    "django.db.backends": {
        "level": "DEBUG",
        "handlers': ["console", ],
    }
}
Enter fullscreen mode Exit fullscreen mode

展示 SQL(第 3 部分)

您还可以打印出 Django 在数据库连接上存储的时间和生成的 SQL。

from django.db import connection

books = Book.objects.all()
print("connection.queries", connection.queries)
Enter fullscreen mode Exit fullscreen mode

一个工具栏控制所有工具栏

如果您的代码是从视图调用的,那么开始解读生成的 SQL 的最简单方法是安装Django Debug Toolbar。DDT 提供了一个非常有用的诊断工具,它可以显示所有正在运行的 SQL 查询,包括哪些查询彼此相似以及哪些查询是重复的。您还可以查看每个 SQL 查询的查询计划,并深入了解其运行缓慢的原因。

选择并预取所有相关

需要注意的是,Django 的 ORM 默认是相当懒惰的。它只有在结果被请求(无论是在代码中还是直接在视图中)时才会运行查询。它也不会在需要时才通过 ForeignKeys 来连接模型。这些都是有益的优化,但如果你没有意识到,它们可能会给你带来麻烦。

# views.py
def index(request):
    books = Book.objects.all()

    return render(request, { "books": books })
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->{% raw %}
{% for book in books %}
Book Author: {{ book.author.name }}<br />
{% endfor %}{% endraw %}
Enter fullscreen mode Exit fullscreen mode

在上面的代码中,列表中的每本书for loop都会index.html再次调用数据库来获取作者姓名。因此,需要先进行一次数据库调用来检索所有书籍的集合,然后再对列表中的每本书进行一次额外的数据库调用。

防止额外数据库调用的方法是select_related强制 Django 加入另一个模型一次,并在使用该关系时防止后续调用。

更新视图代码以使用select_related将使同一 Django 模板的总 SQL 调用数减少到仅 1。

# views.py
def index(request):
    books = Book.objects.select_related("author").all()

    return render(request, { "books": books })
Enter fullscreen mode Exit fullscreen mode

在某些情况下select_related不起作用,但prefetch_related可以。Django 文档有更多关于何时使用 的详细信息prefetch_related

注意模型的实例化

Django ORM 创建模型时,QuerySet会从数据库中检索数据并填充到模型中。但是,如果您不需要模型,可以通过几种方法跳过不必要的模型构建。

values_list将返回所有指定列的元组列表。flat=True如果仅指定一个字段,则关键字参数会返回一个扁平列表。

# get a list of book ids to use later
book_ids = Book.objects.all().values_list("id", flat=True)
Enter fullscreen mode Exit fullscreen mode

您还可以创建一个字典,其中包含稍后可能需要的数据对values。例如,如果我需要博客 ID 及其 URL:

# get a dictionary of book id->title
book_ids_to_titles = {b.get("id"): b.get("title") for b in Book.objects.all().values("id", "title")}
Enter fullscreen mode Exit fullscreen mode

要获取所有书籍 ID:。book_ids_to_titles.keys()要获取所有标题:book_ids_to_titles.values()

有点相关,bidict对于从字典的值中检索字典的键以及反之亦然(而不是保留大约 2 个字典)的简单方法来说非常棒。

book_ids_to_titles = bidict({
    "1": "The Sandman",
    "2": "Good Omens",
    "3": "Coraline",
})

assert book_ids_to_titles["1"] == book_ids_to_titles.inv["The Sandman"]
Enter fullscreen mode Exit fullscreen mode

通过 ID 进行过滤让世界运转起来

使用 会filter转换为WHERESQL 中的子句,并且在 Postgres 中搜索整数几乎总是比搜索字符串更快。因此,Book.objects.filter(id__in=book_ids)的性能会比 略高Book.objects.filter(title__in=book_titles)

只遵从你的心意

Only并且Defer是镜像相反的方法,以实现仅为模型检索特定字段的相同目标。Only通过选择指定的数据库字段来工作,但不填写任何未指定的字段。Defer以相反的方式工作,因此字段将不会包含在 SELECT 语句中。

然而,Django 文档中的这条注释却说明了这一点:

当您仔细分析了查询并准确了解了所需的信息,并测量了返回所需字段和模型的完整字段集之间的差异时,它们会提供优化。

注释并继续

对于某些代码,我循环获取列表中每个模型的计数。

for author in Author.objects.all():
    book_count = author.books.count()
    print(f"{book_count} books by {author.name}")
Enter fullscreen mode Exit fullscreen mode

这会SELECT为每位作者创建一个 SQL 语句。使用annotation则会创建一个 SQL 查询。

author_counts = (
    Author.objects
    .annotate(book_count=Count("book__id"))
    .values("author__name", "book_count")
)

for obj in author_counts:
    print(f"{obj.get('book_count')} books by {obj.get('author__name')}")
Enter fullscreen mode Exit fullscreen mode

Aggregationannotation如果您想要计算列表中所有对象的值(例如从模型列表中获取最大 ID),则是更简单的版本。Annotation如果您想计算列表中每个模型的值并获取输出,则很有用。

批量粉碎!呃,创建

使用 可以通过一个查询创建多个对象bulk_create。使用时有一些注意事项,遗憾的是,您无法获得插入后创建的 ID 列表,而这很有用。不过,对于简单的用例来说,它非常有效。

author = Author(name="Neil Gaiman")
author.save()

Book.objects.bulk_create([
    Book(title="Neverwhere", author=author),
    Book(title="The Graveyard Book", author=author),
    Book(title="The Ocean at the End of Lane", author=author),
])
Enter fullscreen mode Exit fullscreen mode

我们想让增肌

update是 上的一个方法QuerySet,因此您可以使用一个 SQL 查询检索一组对象并更新所有对象上的字段。但是,如果您想更新一组具有不同字段值的模型,django-bulk-update这将非常方便。它会自动为一组模型更新创建一个 SQL 语句,即使它们具有不同的值。

from django.utils import timezone
from django_bulk_update.helper import bulk_update

books = Book.objects.all()
for book in books:
    book.title = f"{book.title} - {timezone.now}"

# generates 1 sql query to update all books
bulk_update(books, update_fields=['title'])
Enter fullscreen mode Exit fullscreen mode

会让你出汗(现在每个人都使用 Raw Sql)

如果您确实无法找到让 Django ORM 生成高性能 SQL 的方法,raw sql那么始终可以使用它,尽管通常不建议使用它,除非您必须这样做。

尽享奢华

Django 文档通常非常有用,它会为您提供有关上述每种技术的更深入的详细信息。如果您知道任何其他可以最大程度提升 Django 性能的方法,我很乐意在@adamghill上与您分享

最初发表于adamghill.com

文章来源:https://dev.to/adamghill/optimize-the-django-orm-53hb