导致 Django 性能下降的问题 ⚠️
Source: Dev.to
索引 📑
- Consultas N+1
- Falta de índices en la base de datos
- Evaluación prematura del QuerySet
- Cargar datos innecesarios (overfetching)
- Falta de
select_relatedyprefetch_related - Uso incorrecto de
count() - Un mal uso de devolver lo mismo que
count() - No usar transacciones correctamente
- No usar bulk operations
- Uso eficiente de
annotate() - No usar Django Debug Toolbar
- No usar Django Silk para profiling
- Linkografía
N+1 查询 🐌
它是与数据库交互的应用程序中常见的性能瓶颈。当应用程序执行 N 条额外查询来获取本可以通过一次查询获得的数据时,就会出现这种情况。这导致总共 N + 1 条查询,而不是仅 1 条,从而在数据集增大时显著降低性能。
示例
for post in Post.objects.all():
print(post.author.name)
这会为 posts 生成 1 条查询 + N 条作者查询。
在 Django 中可以通过添加 select_related 方法来解决:
posts = Post.objects.select_related("author")
for post in posts:
print(post.author.name)
数据库缺少索引 📚
索引 优化了表中信息的检索,使查询更快,无需遍历所有记录。这是大数据量应用中最常见的问题之一。
User.objects.filter(email="test@gmail.com")
没有索引时,会对表进行全表扫描。在生产环境中,面对数百万行数据,会导致性能严重下降。
要在 Django ORM 中添加索引,需要使用 db_index=True:
class User(models.Model):
email = models.EmailField(db_index=True)
对 QuerySet 的过早评估 ⚡
QuerySet 是 lazy,但很多人错误地对其进行评估:
qs = User.objects.all()
if len(qs) > 0:
...
正确的用法是使用 exists() 来检查其是否存在:
if qs.exists():
...
exists() 方法用于检查对象是否存在的查询;如果 QuerySet 包含结果则返回 True,否则返回 False。它以最简单、最快速的方式执行查询,但当你随后需要访问对象时并不适用。
加载不必要的数据(overfetching) 📦
在执行查询时如果只需要部分字段,最好限制检索的内容。
导致过度加载的示例:
users = User.objects.all()
仅获取姓名(以及 id):
-
使用
only()(返回模型实例):User.objects.only("name") -
使用
values()(返回字典):User.objects.values("name") -
使用
values_list()(返回元组列表或平面值):User.objects.values_list("name", flat=True)
缺少 select_related 和 prefetch_related 🔗
访问关联对象可能导致 N+1 查询问题。Django 提供了 select_related() 和 prefetch_related() 来解决该问题并提升性能。
select_related()
通过 JOIN 在单个 SQL 查询中获取关联对象。更适用于 ForeignKey 和 OneToOneField 关系。
Post.objects.select_related("author")
prefetch_related()
执行单独的查询并在 Python 中合并结果。更适用于 ManyToMany、反向 ForeignKey 和反向 OneToOne 关系。
books = Book.objects.prefetch_related('authors')
for book in books:
print(book.title)
for author in book.authors.all():
print(author.name)
使用 Prefetch 的高级用法
当你需要过滤、排序或分配到自定义属性时,使用 Prefetch 对象。
from django.db.models import Prefetch
books = Book.objects.prefetch_related(
Prefetch(
"authors",
queryset=Author.objects.filter(active=True)
)
)
for book in books:
for author in book.authors.all():
print(author.name)
count() 的错误用法 🔢
count() 方法返回一个 整数,表示数据库中与 QuerySet 匹配的对象数量。
User.objects.count()
注意: 如果你只需要知道是否存在记录,
exists()比count()更高效,因为它避免了对所有结果进行计数。
对 count() 的错误使用
len(User.objects.all())
count() 直接在数据库上执行 SELECT COUNT(*),而 len() 会把所有对象加载到内存中。
未正确使用事务 🔒
Django 默认使用 autocommit,这意味着每个 .save()、.create() 或 .update() 操作都会作为一个独立的事务执行。
这有两个重要的后果:
- 由于多次提交导致更大的开销。
- 如果在复杂操作的中途出现错误,可能导致不一致的风险。
常见问题
for item in items:
item.processed = True
item.save()
Django 为每次迭代执行 1 UPDATE = 1 COMMIT。如果有 10 000 条对象,就会有 10 000 次提交 → 不必要的开销和更长的总执行时间。
使用 transaction.atomic() 的解决方案
from django.db import transaction
with transaction.atomic():
for item in items:
item.processed = True
item.save()
现在 Django 执行 10 000 UPDATE = 1 次提交(虽然查询次数没有减少)。
不使用 bulk 操作 🚀
bulk_create() 高效地将提供的对象列表插入数据库,并以列表形式返回创建的对象,顺序与提供时相同:
users = [
User(username="user1"),
User(username="user2"),
]
User.objects.bulk_create(users)
高效使用 annotate() 📊
annotate() 允许使用 SQL 聚合为 QuerySet 的每个对象添加计算后的信息,避免不必要的额外查询。
常见问题
posts = Post.objects.all()
for post in posts:
print(post.title, post.comments.count())
这会产生:
- 1 次查询获取帖子。
- N 次额外查询来统计评论(N+1 问题)。
使用 annotate() 的解决方案
from django.db.models import Count
posts = Post.objects.annotate(comment_count=Count("comments"))
for post in posts:
print(post.title, post.comment_count)
只会执行 一次 SQL 查询,因为计数在数据库中完成。
优势
- 减少查询次数。
- 利用数据库引擎的优化。
- 在大数据量下显著提升性能。
其他有用函数
from django.db.models import Count, Sum, Avg, Max, Min
User.objects.annotate(
post_count=Count("posts"),
avg_score=Avg("posts__score")
)
不使用 Django Debug Toolbar 🔎
django-debug-toolbar 实时显示关于你的应用性能的详细信息,此外还有:
- 已执行的 SQL 查询数量。
- 每个查询的执行时间。
- 重复查询。
- 总响应时间。
- 缓存使用情况。
这可以帮助检测以下问题:
- N+1 查询。
- 不必要的查询。
- 慢查询。
- 缺少索引。
不要使用 Django Silk 进行性能分析 🔬
优化而不进行测量是最常见的错误之一。django-silk 是一种性能分析工具,可分析你的应用程序的真实性能。与 Django Debug Toolbar 不同,Django Silk:
- 保存请求历史。
- 允许详细分析查询。
- 显示精确的执行时间。
- 支持对 Python 函数进行性能分析。