🚀 快速安装

复制以下命令并运行,立即安装此 Skill:

npx skills add https://skills.sh/getsentry/skills/django-perf-review

💡 提示:需要 Node.js 和 NPM

Django 性能审查

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

审查方法

  1. 先研究 – 追踪数据流,检查现有优化措施,验证数据量
  2. 报告前先验证 – 模式匹配不等于验证
  3. 零发现是可以接受的 – 不要为了显得全面而制造问题
  4. 严重性必须与影响匹配 – 如果你在写一个“关键”发现时,却用了“轻微”来形容,那它就不是关键。降级或跳过它。

影响类别

问题按影响程度组织。重点关注关键级别——这些问题在规模下会引发实际故障。

优先级 类别 影响
1 N+1 查询 关键 – 随数据量倍增,导致超时
2 无界查询集 关键 – 内存耗尽,导致 OOM 终止
3 缺少索引 – 对大表进行全表扫描
4 写入循环 – 锁争用,请求缓慢
5 低效模式 – 很少值得报告

优先级 1:N+1 查询(关键)

影响: 每个 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:无界查询集(关键)

影响: 加载整个表会耗尽内存。大表会导致 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:缺少索引(高)

影响: 全表扫描。在小表上可忽略,在大表上是灾难性的。

规则:为大表上 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:写入循环(高)

影响: 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:低效模式(低)

很少值得报告。 如果你已经在报告真正的问题,可以将其作为次要说明包含在内。

模式:count() 与 exists()

# 略微次优
if queryset.count() > 0:
    do_thing()

# 稍微好一点
if queryset.exists():
    do_thing()

通常跳过 – 大多数情况下差异小于 1ms。

模式:len(queryset) 与 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、分页
  3. 验证数据量 – 检查表是否真的很大
  4. 确认热点路径 – 追踪调用点,验证这频繁运行
  5. 排除缓解措施 – 检查是否有缓存、限流

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


输出格式

## Django 性能审查:[文件/组件名称]

### 摘要
已验证的问题:X(Y 个关键,Z 个高)

### 发现

#### [PERF-001] UserListView 中的 N+1 查询(关键)
**位置:** `views.py:45`

**问题:** 在模板循环中访问了相关字段 `profile`,但没有预取。

**验证:**
- 已追踪:UserListView → users 查询集 → user_list.html → 循环中的 `{{ user.profile.bio }}`
- 已搜索代码库:未找到 select_related('profile')
- User 表: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')

如果未发现问题:“在审查 [文件] 并验证了 [检查内容] 后,未识别出性能问题。”

**提交前,对每个发现进行合理性检查:**
- 严重性与实际影响匹配吗?(“微小的低效”≠ 关键)
- 这是真正的性能问题,还是只是风格偏好?
- 修复这个问题会带来可衡量的性能提升吗?

如果任何答案是“否” - 移除该发现。

---

## 不要报告的内容

- 测试文件
- 仅限管理员的视图
- 管理命令
- 迁移文件
- 一次性脚本
- 在禁用功能标志后的代码
- 行数 < 1000 且不会增长的表
- 冷路径中的模式(极少执行的代码)
- 微观优化(exists 与 count,没有证据的 only/defer)

### 要避免的误报

**查询集变量赋值不是问题:**
```python
# 这没问题 - 没有性能差异
projects_qs = Project.objects.filter(org=org)
projects = list(projects_qs)

# 与此相比 - 性能相同
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 - 最多报告为低
state = AutofixState.objects.filter(pr_id=pr_id).first()
project_id = state.request.project_id  # 第二次查询

N+1 需要一个循环。单个对象执行 2 次查询而不是 1 次,如果相关,可以作为低级别报告,但绝不能作为关键/高级别。

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

📄 原始文档

完整文档(英文):

https://skills.sh/getsentry/skills/django-perf-review

💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。