Django性能审查Skill django-perf-review

这个技能用于审查和优化Django框架下的代码性能,专门检测N+1查询、无界查询集、缺少索引等常见性能问题,提供验证方法和修复建议,帮助开发者提升应用效率和稳定性。关键词:Django性能审查,N+1查询优化,数据库性能,查询集优化,Python后端开发。

后端开发 0 次安装 0 次浏览 更新于 3/17/2026

name: django-perf-review description: Django性能代码审查。当被要求"审查Django性能"、“查找N+1查询”、“优化Django”、“检查查询集性能”、"数据库性能"或审计Django代码的性能问题时使用。 allowed-tools: Read, Grep, Glob, Bash, Task license: LICENSE

Django性能审查

审查Django代码以发现已验证的性能问题。在报告之前研究代码库以确认问题。只报告你能证明的。

审查方法

  1. 先研究 - 跟踪数据流,检查现有优化,验证数据量
  2. 报告前验证 - 模式匹配不是验证
  3. 零发现是可接受的 - 不要制造问题以显得彻底
  4. 严重性必须匹配影响 - 如果你发现自己在一个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)

仅标志如果循环大或这在非常热路径中。


验证要求

报告任何问题前:

  1. 跟踪数据流 - 从创建到消费跟踪查询集
  2. 搜索现有优化 - Grep select_related, prefetch_related, pagination
  3. 验证数据量 - 检查表是否实际大
  4. 确认热路径 - 跟踪调用点,验证这频繁运行
  5. 排除缓解措施 - 检查缓存,速率限制

如果不能验证所有步骤,不要报告。


输出格式

## 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。

风格偏好不是性能问题: 如果你的唯一建议是"合并这两行"或"重命名此变量" - 那是风格,不是性能。不要报告。