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

2025-06-04

优化 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
PREV
我们不知道 React 状态钩子是如何工作的
NEXT
如何在 Vue3 应用程序中构建身份验证 如何在 Vue3 应用程序中构建身份验证