name: django-perf-review description: Django性能代码审查。当被要求"审查Django性能"、“查找N+1查询”、“优化Django”、“检查查询集性能”、"数据库性能"或审计Django代码的性能问题时使用。 allowed-tools: Read, Grep, Glob, Bash, Task license: LICENSE
Django性能审查
审查Django代码以发现已验证的性能问题。在报告之前研究代码库以确认问题。只报告你能证明的。
审查方法
- 先研究 - 跟踪数据流,检查现有优化,验证数据量
- 报告前验证 - 模式匹配不是验证
- 零发现是可接受的 - 不要制造问题以显得彻底
- 严重性必须匹配影响 - 如果你发现自己在一个CRITICAL发现中写"minor",那就不关键。降级或跳过。
影响类别
问题按影响组织。专注于CRITICAL和HIGH - 这些在规模上导致真正的问题。
| 优先级 | 类别 | 影响 |
|---|---|---|
| 1 | N+1查询 | CRITICAL - 随数据倍增,导致超时 |
| 2 | 无界查询集 | CRITICAL - 内存耗尽,OOM杀死 |
| 3 | 缺少索引 | HIGH - 在大表上进行全表扫描 |
| 4 | 写入循环 | HIGH - 锁争用,慢请求 |
| 5 | 低效模式 | LOW - 很少值得报告 |
优先级1:N+1查询(CRITICAL)
影响: 每个N+1添加O(n)数据库往返。100行 = 100额外查询。10,000行 = 超时。
规则:预取循环中访问的相关数据
通过跟踪验证:视图 → 查询集 → 模板/序列化器 → 循环访问
# 问题:N+1 - 每次迭代查询profile
def user_list(request):
users = User.objects.all()
return render(request, 'users.html', {'users': users})
# 模板:
# {% for user in users %}
# {{ user.profile.bio }} ← 每个用户触发查询
# {% endfor %}
# 解决方案:在视图中预取
def user_list(request):
users = User.objects.select_related('profile')
return render(request, 'users.html', {'users': users})
规则:在序列化器中预取,不仅仅是视图
DRF序列化器访问相关字段如果查询集未优化会导致N+1。
# 问题:SerializerMethodField每个对象查询
class UserSerializer(serializers.ModelSerializer):
order_count = serializers.SerializerMethodField()
def get_order_count(self, obj):
return obj.orders.count() # ← 每个用户查询
# 解决方案:在视图集中注释,在序列化器中访问
class UserViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return User.objects.annotate(order_count=Count('orders'))
class UserSerializer(serializers.ModelSerializer):
order_count = serializers.IntegerField(read_only=True)
规则:查询的模型属性在循环中是危险的
# 问题:属性在访问时触发查询
class User(models.Model):
@property
def recent_orders(self):
return self.orders.filter(created__gte=last_week)[:5]
# 在模板循环中使用 = N+1
# 解决方案:使用带有自定义查询集的Prefetch,或注释
N+1的验证清单
- [ ] 从视图到模板/序列化器跟踪数据流
- [ ] 确认相关字段在循环中访问
- [ ] 搜索代码库现有select_related/prefetch_related
- [ ] 验证表有显著行数(1000+)
- [ ] 确认这是热路径(不是管理界面,不罕见操作)
优先级2:无界查询集(CRITICAL)
影响: 加载整个表耗尽内存。大表导致OOM杀死和工作器重启。
规则:总是分页列表端点
# 问题:无分页 - 加载所有行
class UserListView(ListView):
model = User
template_name = 'users.html'
# 解决方案:添加分页
class UserListView(ListView):
model = User
template_name = 'users.html'
paginate_by = 25
规则:对大批量处理使用iterator()
# 问题:一次性将所有对象加载到内存
for user in User.objects.all():
process(user)
# 解决方案:用iterator()流式处理
for user in User.objects.iterator(chunk_size=1000):
process(user)
规则:永远不要在无界查询集上调用list()
# 问题:强制全评估到内存
all_users = list(User.objects.all())
# 解决方案:保持为查询集,如果需要切片
users = User.objects.all()[:100]
无界查询集的验证清单
- [ ] 表大(10k+行)或将无限增长
- [ ] 无分页类,paginate_by,或切片
- [ ] 这在用户面向请求上运行(不是有分块的后台作业)
优先级3:缺少索引(HIGH)
影响: 全表扫描。小表上可忽略,大表上灾难性。
规则:索引大表上WHERE子句使用的字段
# 问题:过滤未索引字段
# User.objects.filter(email=email) # 如果没有索引,全扫描
class User(models.Model):
email = models.EmailField() # ← 无db_index
# 解决方案:添加索引
class User(models.Model):
email = models.EmailField(db_index=True)
规则:索引大表上ORDER BY使用的字段
# 问题:排序无索引需要全扫描
Order.objects.order_by('-created')
# 解决方案:索引排序字段
class Order(models.Model):
created = models.DateTimeField(db_index=True)
规则:对常见查询模式使用复合索引
class Order(models.Model):
user = models.ForeignKey(User)
status = models.CharField(max_length=20)
created = models.DateTimeField()
class Meta:
indexes = [
models.Index(fields=['user', 'status']), # 用于filter(user=x, status=y)
models.Index(fields=['status', '-created']), # 用于filter(status=x).order_by('-created')
]
缺少索引的验证清单
- [ ] 表有10k+行
- [ ] 字段在热路径上用于filter()或order_by()
- [ ] 检查模型 - 无db_index=True或Meta.indexes条目
- [ ] 不是外键(已自动索引)
优先级4:写入循环(HIGH)
影响: N数据库写入而不是1。锁争用。慢请求。
规则:在循环中使用bulk_create而不是create()
# 问题:N插入,N往返
for item in items:
Model.objects.create(name=item['name'])
# 解决方案:单个批量插入
Model.objects.bulk_create([
Model(name=item['name']) for item in items
])
规则:使用update()或bulk_update而不是在循环中save()
# 问题:N更新
for obj in queryset:
obj.status = 'done'
obj.save()
# 解决方案A:单个UPDATE语句(所有相同值)
queryset.update(status='done')
# 解决方案B:bulk_update(不同值)
for obj in objects:
obj.status = compute_status(obj)
Model.objects.bulk_update(objects, ['status'], batch_size=500)
规则:在查询集上使用delete(),而不是在循环中
# 问题:N删除
for obj in queryset:
obj.delete()
# 解决方案:单个DELETE
queryset.delete()
写入循环的验证清单
- [ ] 循环迭代100+项(或无界)
- [ ] 每次迭代调用create(), save(), 或delete()
- [ ] 这在用户面向请求上运行(不是一次性迁移脚本)
优先级5:低效模式(LOW)
很少值得报告。 仅作为次要注释,如果你已经报告真正问题。
模式:count() vs exists()
# 稍次优
if queryset.count() > 0:
do_thing()
# 边际更好
if queryset.exists():
do_thing()
通常跳过 - 大多数情况下差异<1ms。
模式:len(queryset) vs count()
# 提取所有行来计数
if len(queryset) > 0: # 如果查询集未评估,坏
# 单个COUNT查询
if queryset.count() > 0:
仅标志如果查询集大且未已评估。
模式:小循环中的get()
# N查询,但如果N小(< 20),通常可以
for id in ids:
obj = Model.objects.get(id=id)
仅标志如果循环大或这在非常热路径中。
验证要求
报告任何问题前:
- 跟踪数据流 - 从创建到消费跟踪查询集
- 搜索现有优化 - Grep select_related, prefetch_related, pagination
- 验证数据量 - 检查表是否实际大
- 确认热路径 - 跟踪调用点,验证这频繁运行
- 排除缓解措施 - 检查缓存,速率限制
如果不能验证所有步骤,不要报告。
输出格式
## Django性能审查:[文件/组件名称]
### 总结
已验证问题:X(Y Critical,Z High)
### 发现
#### [PERF-001] N+1查询在UserListView(CRITICAL)
**位置:** `views.py:45`
**问题:** 相关字段`profile`在模板循环中访问未预取。
**验证:**
- 跟踪:UserListView → 用户查询集 → user_list.html → `{{ user.profile.bio }}` 在循环中
- 搜索代码库:未找到select_related('profile')
- 用户表:50k+行(在管理界面验证)
- 热路径:从主页导航链接
**证据:**
```python
def get_queryset(self):
return User.objects.filter(active=True) # 无select_related
修复:
def get_queryset(self):
return User.objects.filter(active=True).select_related('profile')
如果未发现问题:“在审查[文件]和验证[你检查的]后未识别性能问题。”
提交前,理智检查每个发现:
- 严重性是否匹配实际影响?(“Minor inefficiency” ≠ CRITICAL)
- 这是真正的性能问题还是只是风格偏好?
- 修复这会可测量地提高性能吗?
如果任何答案是"否" - 移除发现。
不要报告什么
- 测试文件
- 仅管理界面视图
- 管理命令
- 迁移文件
- 一次性脚本
- 禁用特性标志后的代码
- 行数<1000且不会增长的表
- 冷路径中的模式(很少执行代码)
- 微优化(exists vs count,only/defer无证据)
避免的错误阳性
查询集变量赋值不是问题:
# 这可以 - 无性能差异
projects_qs = Project.objects.filter(org=org)
projects = list(projects_qs)
# vs 这 - 相同性能
projects = list(Project.objects.filter(org=org))
查询集是惰性的。赋值给变量不执行任何事。
单查询模式不是N+1:
# 这是一个查询,不是N+1
projects = list(Project.objects.filter(org=org))
N+1需要循环触发额外查询。单个list()调用可以。
单个对象获取缺少select_related不是N+1:
# 这是2查询,不是N+1 - 最多报告为LOW
state = AutofixState.objects.filter(pr_id=pr_id).first()
project_id = state.request.project_id # 第二查询
N+1需要循环。单个对象做2查询而不是1,如果相关可以报告为LOW,但从不作为CRITICAL/HIGH。
风格偏好不是性能问题: 如果你的唯一建议是"合并这两行"或"重命名此变量" - 那是风格,不是性能。不要报告。