常见的 Django ORM 错误修复

Django ORM 是 Django 最强大的功能之一。它抽象了与数据库交互的大部分复杂性,让开发人员可以使用 Pythonic 语法而不是原始 SQL 来操作数据。所有这些 ORM 函数都会生成 SQL 查询,如果处理不当,这些查询可能会成为瓶颈。

本博客重点介绍了使用 Django ORM 时常见的错误,并提供了保持查询高效、可维护和高性能的技巧。

1. N+1 查询问题

当您的代码触发一个查询来获取一组记录,然后再次运行 N 个附加查询来获取相关数据时,就会发生 N+1 查询问题。

blogs = Blog.objects.all()    # 1 Query
for blog in blogs:
    print(blog.author.name)   # N additional queries

在上面的例子中,访问循环内的 blog.author.name 会导致 Django 单独获取每个博客的作者记录,从而产生 N 个额外的查询。

**如何修复**

对于单个相关对象(例如 ForeignKey 或 OneToOneField),请使用 **select_related**,因为它会执行 SQL JOIN 以在一个查询中检索主对象及其相关对象。对于多对多、多对一或反向关系,请使用 **prefetch_related**,它会在单独的查询中获取相关数据,但在 Python 中有效地将它们组合在一起,从而避免 N+1 问题。

# With select_related
blogs = Blog.objects.select_related('author').all()

# With prefetch_related
authors = Author.objects.prefetch_related('blogs').all()

2. 过度使用 .all() 和 .filter()

开发人员经常链接多个过滤器或使用 .all() 然后对同一个查询集进行重复查询:

blogs = Blog.objects.all()
active_blogs = blogs.filter(is_archived=False)
popular_blogs = blogs.filter(views__gte=1000)

尽管 Django 尝试通过仅在需要时延迟评估查询集来优化查询集,但在同一查询集数据上重复调用过滤器仍然会导致对数据库造成不必要的访问。

**如何修复**

在一个语句中组合过滤器允许 django 生成单个 SQL 查询。

popular_active_blogs = Blog.objects.filter(is_archived=False, views__gte=1000)

3. 没有利用 values() 或 values_list()

有时候我们只需要模型中特定的字段而不是所有字段的数据,此时使用**.values()**或**.values_list()**会更加高效。

titles = Blog.objects.values('title')
or
titles = Blog.objects.values_list('title', flat=True)
# values() returns a list of dictionaries.
# values_list() can return tuples or flat values if flat=True is provided.

通过仅获取所需的列,您可以减少从数据库传输的数据量,从而提高性能。

4. 聚合和注释效率低下

重复调用**.aggregate()**或**.annotate()**可能会导致多次查询。带有多个注释的复杂查询可能会导致 SQL 查询效率低下,从而导致繁重的数据库操作。

# Example of multiple aggregate
total_count = Blog.objects.aggregate(Count('id'))
author_count = Blog.objects.aggregate(Count('author'))
average_views = Blog.objects.aggregate(Avg('views'))

**推荐**

stats = Blog.objects.aggregate(
    total_count=Count('id'), 
    author_count=Count('author'),
    avg_average_views =Avg('views')
)

5. 不使用数据库索引

索引可使数据库快速定位和检索数据,从而避免缓慢的全表扫描,从而提高查询性能。索引可优化过滤、排序和连接等操作,使对经常访问的字段的查询速度更快。如果经常查询的字段缺少数据库索引,则可能会大大降低性能。

**如何在 Django 中添加索引**

# Model Field Index
class Blog(models.Model):
    title = models.CharField(max_length=255, db_index=True)
    slug = models.SlugField(max_length=255, db_index=True)

# Meta Indexes
class Blog(models.Model):
    title = models.CharField(max_length=255)
    views = models.IntegerField(default=0)

    class Meta:
        indexes = [
            models.Index(fields=['title', 'views']),
        ]

索引可以加快读取速度,但会降低写入速度。因此,只索引那些经常需要查询的字段。

6. 不使用缓存

当我们需要查询计算成本高昂或很少更改的数据时,请使用缓存。即使缓存 5 分钟也可以节省重复查询、复杂计算和不频繁更改的查询。

from django.core.cache import cache

def get_popular_blogs():
    popular_blogs = cache.get('popular_blogs_cache_key')
    if popular_blogs is None:
        popular_blogs = Blog.objects.filter(views__gte=1000)
        cache.set('popular_blogs_cache_key', popular_blogs, 300)
    return popular_blogs

7. 原始 SQL

有时,Django ORM 无法有效地表达复杂的查询或批量操作。虽然 Django 提供了 .extra() 或 .raw(),但原始 SQL 的使用应该是最后的手段,因为它:

  • 失去了 ORM 的许多好处
  • 可能导致代码难以阅读或容易出错
  • 确保输入得到正确清理,并保持原始 SQL 查询可维护。

    应用这些技巧,您将提高 Django 应用程序的性能,同时保持代码干净且易于维护。还建议在您的开发环境中使用 **Django 调试工具栏** 来监视和分析执行了多少查询、它们的执行时间和 SQL 语句。