导致 Django 性能下降的问题 ⚠️

发布: (2026年3月1日 GMT+8 04:11)
8 分钟阅读
原文: Dev.to

Source: Dev.to

索引 📑

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 的过早评估 ⚡

QuerySetlazy,但很多人错误地对其进行评估:

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)

访问关联对象可能导致 N+1 查询问题。Django 提供了 select_related()prefetch_related() 来解决该问题并提升性能。

通过 JOIN 在单个 SQL 查询中获取关联对象。更适用于 ForeignKeyOneToOneField 关系。

Post.objects.select_related("author")

执行单独的查询并在 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 函数进行性能分析。

链接文献 📚

0 浏览
Back to Blog

相关文章

阅读更多 »

不糟糕的语义失效

缓存问题 如果你在 Web 应用上工作了一段时间,你就会了解缓存的情况。你加入缓存,一切都变快了,然后有人……