一、为什么Django ORM查询会变慢

咱们先聊聊为什么用着用着就发现Django ORM变慢了。其实ORM就像是个翻译官,把我们的Python代码翻译成SQL语句。但有时候这个翻译官太尽职了,反而把事情搞复杂了。

举个例子,我们有个博客系统,要显示文章列表和每篇文章的评论数:

# 技术栈:Django 3.2 + PostgreSQL
# 不优化的写法
articles = Article.objects.all()
for article in articles:
    comments_count = article.comments.count()  # 每次循环都执行一次查询

这样写的话,如果有100篇文章,就会产生101次数据库查询(1次取文章,100次查评论数)。这就是臭名昭著的"N+1查询问题"。

二、预加载关联数据是王道

解决上面问题最直接的方法就是用select_relatedprefetch_related这两个神器。

# 优化后的写法
articles = Article.objects.select_related('author').prefetch_related('comments')
for article in articles:
    comments_count = article.comments.count()  # 这里不会产生额外查询

select_related适合一对一或多对一关系,它通过JOIN一次性获取数据。而prefetch_related适合多对多或一对多关系,它会先执行主查询,再执行一个额外的查询来获取关联数据。

三、只取你需要的字段

有时候我们只需要几个字段,但ORM默认会取所有字段。这时候可以用onlydefer来优化。

# 只需要标题和发布时间
articles = Article.objects.only('title', 'publish_date')

# 排除大文本字段
articles = Article.objects.defer('content')

但要注意,如果后面用到了被排除的字段,Django会再发起一次查询,这就得不偿失了。

四、聚合查询替代循环计算

统计类查询特别适合用聚合函数,而不是在Python中循环计算。

from django.db.models import Count

# 统计每篇文章的评论数(一次查询搞定)
articles = Article.objects.annotate(comment_count=Count('comments'))
for article in articles:
    print(article.comment_count)  # 直接使用预计算的统计值

五、数据库索引不能忘

再好的ORM优化也抵不过糟糕的数据库设计。确保你的常用查询字段都有索引:

class Article(models.Model):
    title = models.CharField(max_length=100, db_index=True)  # 添加索引
    publish_date = models.DateTimeField(db_index=True)  # 日期也加索引

但索引不是越多越好,它会增加写入时的开销。通常只为常用的查询条件和排序字段加索引。

六、批量操作代替单个操作

需要创建或更新大量数据时,记得用批量操作:

# 糟糕的做法
for i in range(100):
    Article.objects.create(title=f'Article {i}')

# 优化的做法
Article.objects.bulk_create(
    [Article(title=f'Article {i}') for i in range(100)]
)

批量操作可以减少数据库往返次数,通常能提升几十倍的性能。

七、原生SQL不是洪水猛兽

当ORM实在搞不定复杂查询时,别害怕写原生SQL:

from django.db import connection

def get_complex_report():
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT a.title, COUNT(c.id) as comment_count
            FROM blog_article a
            LEFT JOIN blog_comment c ON a.id = c.article_id
            GROUP BY a.id
            HAVING COUNT(c.id) > 10
        """)
        return dictfetchall(cursor)

八、缓存是最后的杀手锏

对于变化不频繁但访问频繁的数据,缓存是终极解决方案:

from django.core.cache import cache

def get_popular_articles():
    key = 'popular_articles'
    articles = cache.get(key)
    if not articles:
        articles = list(Article.objects.filter(is_popular=True))
        cache.set(key, articles, timeout=3600)  # 缓存1小时
    return articles

九、监控和分析查询性能

最后,要养成监控查询性能的习惯:

from django.db import connection
from django.conf import settings

if settings.DEBUG:
    print(len(connection.queries))  # 打印执行的查询数量
    for query in connection.queries:
        print(query['sql'])  # 打印每条SQL语句

十、实战经验总结

  1. 预加载是解决N+1问题的银弹
  2. 只查询需要的字段
  3. 聚合优于循环计算
  4. 合适的索引事半功倍
  5. 批量操作大幅提升性能
  6. 必要时使用原生SQL
  7. 合理使用缓存减轻数据库压力
  8. 持续监控查询性能

记住,没有放之四海而皆准的优化方案,关键是要理解原理,根据实际场景选择合适的策略。优化时要测量,不要猜测,用数据说话才是王道。