一、为什么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_related和prefetch_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默认会取所有字段。这时候可以用only和defer来优化。
# 只需要标题和发布时间
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语句
十、实战经验总结
- 预加载是解决N+1问题的银弹
- 只查询需要的字段
- 聚合优于循环计算
- 合适的索引事半功倍
- 批量操作大幅提升性能
- 必要时使用原生SQL
- 合理使用缓存减轻数据库压力
- 持续监控查询性能
记住,没有放之四海而皆准的优化方案,关键是要理解原理,根据实际场景选择合适的策略。优化时要测量,不要猜测,用数据说话才是王道。
评论