一、什么是 N+1 查询问题?
📌 场景示例
假设你有以下模型:
# models.py
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
现在你想在模板中列出所有书籍及其作者:
# views.py
books = Book.objects.all()
<!-- template.html -->
{% for book in books %}
<p>{{ book.title }} by {{ book.author.name }}</p>
{% endfor %}
🔍 发生了什么?
- 1 次查询:
SELECT * FROM book;(获取所有书籍) - N 次查询:对每本书,执行
SELECT * FROM author WHERE id = ?;(获取作者)
如果 books 有 100 条记录 → 总共 101 次数据库查询!
✅ 这就是 N+1 问题:1 次主查询 + N 次关联查询。
二、如何诊断 N+1 问题?
方法 1:使用 django.db.connection.queries
from django.db import connection
books = Book.objects.all()
for book in books:
print(book.author.name)
print(f"Total queries: {len(connection.queries)}")
⚠️ 注意:需关闭
DEBUG=False或手动启用:from django.conf import settings settings.DEBUG = True
方法 2:使用 Django Debug Toolbar(开发环境必备)
- 安装后,页面底部显示 SQL 查询次数和内容
- 红色高亮重复查询
方法 3:日志监控(生产环境)
配置数据库日志,观察短时间内的高频相似查询。
三、解决方案(按场景分类)
✅ 方案 1:使用 select_related()(适用于 ForeignKey / OneToOne)
原理:通过 JOIN 在一次查询中获取关联对象。
# 修复前(N+1)
books = Book.objects.all()
# 修复后(1 次查询)
books = Book.objects.select_related('author')
生成 SQL:
SELECT book.*, author.*
FROM book
LEFT OUTER JOIN author ON book.author_id = author.id;
支持多层关联:
# 获取书籍 → 作者 → 作者的国家
books = Book.objects.select_related('author__country')
✅ 适用关系:正向 ForeignKey、OneToOneField
✅ 方案 2:使用 prefetch_related()(适用于 反向 ForeignKey / ManyToMany)
原理:先查主表,再用 IN 查询 一次性获取所有关联对象,然后在 Python 中“拼接”。
# models.py
class Publisher(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = = models.CharField(max_length=100)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
想列出每个出版社及其所有书籍:
# 视图
publishers = Publisher.objects.prefetch_related('book_set')
{% for publisher in publishers %}
<h3>{{ publisher.name }}</h3>
<ul>
{% for book in publisher.book_set.all %}
<li>{{ book.title }}</li>
{% endfor %}
</ul>
{% endfor %}
✅ 只产生 2 次查询:
SELECT * FROM publisher;SELECT * FROM book WHERE publisher_id IN (1, 2, 3, ...);
支持复杂预取:
# 预取多层 + 过滤
publishers = Publisher.objects.prefetch_related(
Prefetch('book_set', queryset=Book.objects.filter(title__icontains='Django'))
)
✅ 适用关系:反向 ForeignKey、ManyToManyField、GenericForeignKey
✅ 方案 3:组合使用 select_related + prefetch_related
# 书籍 → 作者(FK) + 标签(M2M)
books = Book.objects.select_related('author').prefetch_related('tags')
✅ 方案 4:使用 Prefetch 对象精细控制
from django.db.models import Prefetch
books = Book.objects.prefetch_related(
Prefetch(
'author',
queryset=Author.objects.only('id', 'name') # 只取必要字段
)
)
避免加载大字段(如 bio、avatar),提升性能。
✅ 方案 5:在序列化器中处理(DRF 场景)
如果你用 Django REST Framework:
# serializers.py
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer() # 默认会触发 N+1!
class Meta:
model = Book
fields = ['id', 'title', 'author']
修复方式:
-
在 ViewSet 中重写
get_queryset:def get_queryset(self): return Book.objects.select_related('author') -
或使用
to_representation缓存(不推荐)
💡 更高级方案:使用
django-restql支持动态字段 + 自动优化。
四、高级技巧与陷阱规避
🔸 技巧 1:只取需要的字段(only() / defer())
# 不要 SELECT *
books = Book.objects.select_related('author').only(
'title', 'author__name'
)
减少网络传输和内存占用。
🔸 技巧 2:避免在循环中触发查询
# ❌ 危险!
for book in Book.objects.all():
if book.author.name == "Alice": # 每次都查 DB!
...
# ✅ 正确
for book in Book.objects.select_related('author'):
if book.author.name == "Alice":
...
🔸 陷阱 1:prefetch_related 后不能再链式过滤
# ❌ 无效!会重新查询
books = Book.objects.prefetch_related('tags')
for book in books:
book.tags.filter(name='Python') # 忽略 prefetch,发起新查询!
# ✅ 正确:在 Prefetch 中过滤
books = Book.objects.prefetch_related(
Prefetch('tags', queryset=Tag.objects.filter(name='Python'))
)
🔸 陷阱 2:select_related 不适用于反向关系
# ❌ 无效
Author.objects.select_related('book') # Book 是反向 FK
# ✅ 应用 prefetch_related
Author.objects.prefetch_related('book_set')
五、自动化检测工具
1. nplusone(强烈推荐!)
自动检测 N+1 并抛出警告或异常。
安装:
pip install nplusone
配置 settings.py:
INSTALLED_APPS += ['nplusone.ext.django']
MIDDLEWARE += ['nplusone.ext.django.NPlusOneMiddleware']
NPLUSONE_RAISE = True # 开发环境直接报错
✅ 效果:一旦出现 N+1,立即抛出
NPlusOneError,强制修复!
2. Django Debug Toolbar
可视化查看所有 SQL 查询,高亮重复语句。
六、总结:最佳实践清单
| 场景 | 推荐方法 |
|---|---|
| 正向 ForeignKey / OneToOne | select_related() |
| 反向 ForeignKey / ManyToMany | prefetch_related() |
| 需要过滤关联对象 | Prefetch(queryset=...) |
| 只取部分字段 | 结合 only() |
| DRF 序列化 | 在 get_queryset 中优化 |
| 开发阶段 | 启用 nplusone + Debug Toolbar |
| 生产监控 | 记录 SQL 查询次数,设置告警 |
💡 黄金法则:
“永远不要在循环中访问关联对象,除非你确定它已被预取。”
七、延伸阅读
- Django 官方文档:Database access optimization
- 《Two Scoops of Django》第 9 章:QuerySets
- GitHub:
nplusone