本文目录导读:
- 使用
select_related解决外键/一对一关系的 N+1 问题 - 使用
prefetch_related解决一对多/多对多关系的 N+1 问题 - 只取需要的字段 ——
only与defer - 分页与限制结果集
- 合理建立数据库索引
- 善用
exists()/count()/update()代替一次性查询 - 避免在循环中执行数据库查询 —— 批量操作
- 使用
select_for_update处理并发(性能+数据一致性) - 结合
connection.queries分析真实 SQL - 完整优化流程总结
在 Django 中,ORM 提供了非常方便的数据操作接口,但如果不注意性能策略,很容易产生 N+1 查询、大量无用字段传输或 重复查询 等问题,下面系统性地介绍几种最常用且有效的优化方法。
使用 select_related 解决外键/一对一关系的 N+1 问题
适用场景:需要访问外键或多对一关系中的关联对象。
错误示例:
循环中访问关联字段会导致多次查询。
books = Book.objects.all()
for book in books:
print(book.author.name) # 每次循环都查询 author 表
优化后:
books = Book.objects.select_related('author').all()
for book in books:
print(book.author.name) # 只有一次 JOIN 查询
select_related 通过 SQL JOIN 一次性取回关联对象的数据,适用于 ForeignKey 和 OneToOneField。
使用 prefetch_related 解决一对多/多对多关系的 N+1 问题
适用场景:需要访问反向关系、多对多关系或复杂筛选条件。
错误示例:
authors = Author.objects.all()
for author in authors:
books = author.book_set.all() # 每个 author 都会触发一次查询
优化后:
authors = Author.objects.prefetch_related('book_set').all()
for author in authors:
books = author.book_set.all() # 只有两次查询(一次查 author,一次查 book)
prefetch_related 在 Python 层面缓存结果,且可以链式使用:
Actor.objects.prefetch_related('movie_set', 'movie_set__director')
只取需要的字段 —— only 与 defer
问题:QuerySet 默认查询所有字段,可能传输大量不必要的数据。
only:只加载指定字段,其他字段在访问时触发额外查询(需注意)。
# 只需要 title 和 price
books = Book.objects.only('title', 'price')
defer:延迟加载指定字段,其他字段正常加载。
# 大部分字段都需加载,仅 content 很少用到
books = Book.objects.defer('content')
推荐:在列表/API 返回场景中,使用 values() 或 values_list() 直接获取字典/元组,完全不构造模型实例,效率更高:
books = Book.objects.values('title', 'price')
分页与限制结果集
Django 的 QuerySet 是惰性的,但执行查询后还是会取回所有匹配的结果。
-
使用
iterator()处理大量数据,避免一次加载进内存:for book in Book.objects.iterator(chunk_size=500): ... -
使用分页器或手动
LIMIT:books = Book.objects.all()[:20] # 只取 20 条 books = Book.objects.all()[10:30] # offset + limit
合理建立数据库索引
ORM 查询的 WHERE / ORDER BY / JOIN 条件如果没有索引配合,即使 SQL 次数少也可能很慢。
在 Meta 中定义索引(Django 3.2+ 支持功能索引):
class Book(models.Model):= models.CharField(max_length=200)
published_date = models.DateField()
class Meta:
indexes = [
models.Index(fields=['published_date']),
models.Index(fields=['title', 'published_date']),
]
也可在字段上设置 db_index=True。
善用 exists() / count() / update() 代替一次性查询
- 判断是否存在:
if Book.objects.filter(...).exists()比if Book.objects.filter(...)快得多。 - 计数:
Book.objects.filter(...).count()使用SELECT COUNT(*)而非取回全部对象。 - 批量更新:
Book.objects.filter(published=True).update(price=0) # 而不是: for book in Book.objects.filter(published=True): book.price = 0 book.save()
同理还有 F() 表达式做原子更新:
from django.db.models import F
Book.objects.update(price=F('price') * 1.1)
避免在循环中执行数据库查询 —— 批量操作
反例:
for name in author_names:
Author.objects.create(name=name)
正例:
authors = [Author(name=name) for name in author_names] Author.objects.bulk_create(authors) # 一次 INSERT 多条
更新也支持 bulk_update:
books = Book.objects.all()[:100]
for book in books:
book.price = 10
Book.objects.bulk_update(books, ['price']) # 一条 UPDATE 多条
使用 select_for_update 处理并发(性能+数据一致性)
如果要在事务中加锁,避免并发修改:
from django.db import transaction
with transaction.atomic():
book = Book.objects.select_for_update().get(id=1)
book.stock -= 1
book.save()
结合 connection.queries 分析真实 SQL
Django 提供调试工具,可以在 shell 或日志中查看生成的 SQL 数量:
from django.db import connection
# 执行查询前重置
connection.queries_log.clear()
# 执行你的 ORM 操作
# ...
# 查看执行的 SQL 条数和内容
for query in connection.queries:
print(query['sql'], query['time'])
此外推荐使用 django-debug-toolbar 在浏览器侧边栏直接查看。
完整优化流程总结
- 确认瓶颈 → 使用
connection.queries或 django-debug-toolbar 分析 N+1 和重复查询。 - 消除 N+1 → 用
select_related/prefetch_related。 - 减少数据传输 →
only/defer/values。 - 减少查询次数 →
bulk_create/bulk_update/ 批量删除。 - 数据库层优化 → 加索引,合理分页。
- Django 特定优化 →
exists()/count()/F()/update()避免加载完整对象。
掌握这些技巧后,大多数 Django 项目的数据库性能问题都能得到显著改善。