优化 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)
展示 SQL(第一部分)
由于 SQL 调用被抽象到一个简单的 API 后面,因此最终很容易产生比你意识到的更多的 SQL 调用。你可以使用 QuerySet 上的属性来检索近似值query
,但要注意它是一种“不透明表示”。
books = Book.objects.all()
print("books.query", books.query)
展示 SQL(第 2 部分)
您还可以将其添加django.db.logging
到已配置的记录器中,以查看生成的 SQL 是否打印到控制台。
"loggers": {
"django.db.backends": {
"level": "DEBUG",
"handlers': ["console", ],
}
}
展示 SQL(第 3 部分)
您还可以打印出 Django 在数据库连接上存储的时间和生成的 SQL。
from django.db import connection
books = Book.objects.all()
print("connection.queries", connection.queries)
一个工具栏控制所有工具栏
如果您的代码是从视图调用的,那么开始解读生成的 SQL 的最简单方法是安装Django Debug Toolbar。DDT 提供了一个非常有用的诊断工具,它可以显示所有正在运行的 SQL 查询,包括哪些查询彼此相似以及哪些查询是重复的。您还可以查看每个 SQL 查询的查询计划,并深入了解其运行缓慢的原因。
选择并预取所有相关
需要注意的是,Django 的 ORM 默认是相当懒惰的。它只有在结果被请求(无论是在代码中还是直接在视图中)时才会运行查询。它也不会在需要时才通过 ForeignKeys 来连接模型。这些都是有益的优化,但如果你没有意识到,它们可能会给你带来麻烦。
# views.py
def index(request):
books = Book.objects.all()
return render(request, { "books": books })
<!-- index.html -->{% raw %}
{% for book in books %}
Book Author: {{ book.author.name }}<br />
{% endfor %}{% endraw %}
在上面的代码中,列表中的每本书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 })
在某些情况下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)
您还可以创建一个字典,其中包含稍后可能需要的数据对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")}
要获取所有书籍 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"]
通过 ID 进行过滤让世界运转起来
使用 会filter
转换为WHERE
SQL 中的子句,并且在 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}")
这会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')}")
Aggregation
annotation
如果您想要计算列表中所有对象的值(例如从模型列表中获取最大 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),
])
我们想让你增肌
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'])
会让你出汗(现在每个人都使用 Raw Sql)
如果您确实无法找到让 Django ORM 生成高性能 SQL 的方法,raw sql
那么始终可以使用它,尽管通常不建议使用它,除非您必须这样做。
尽享奢华
Django 文档通常非常有用,它会为您提供有关上述每种技术的更深入的详细信息。如果您知道任何其他可以最大程度提升 Django 性能的方法,我很乐意在@adamghill上与您分享。
最初发表于adamghill.com。
文章来源:https://dev.to/adamghill/optimize-the-django-orm-53hb