🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx skills add https://skills.sh/getsentry/skills/django-perf-review
💡 提示:需要 Node.js 和 NPM
Django 性能审查
审查 Django 代码以发现已验证的性能问题。在报告前,先研究代码库以确认问题。只报告你能证明的问题。
审查方法
- 先研究 – 追踪数据流,检查现有优化措施,验证数据量
- 报告前先验证 – 模式匹配不等于验证
- 零发现是可以接受的 – 不要为了显得全面而制造问题
- 严重性必须与影响匹配 – 如果你在写一个“关键”发现时,却用了“轻微”来形容,那它就不是关键。降级或跳过它。
影响类别
问题按影响程度组织。重点关注关键和高级别——这些问题在规模下会引发实际故障。
| 优先级 | 类别 | 影响 |
|---|---|---|
| 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)
仅在循环很大或这处于非常热点的路径上时标记。
验证要求
在报告任何问题之前:
- 追踪数据流 – 跟随查询集从创建到消费
- 搜索现有优化 – 用 grep 搜索 select_related、prefetch_related、分页
- 验证数据量 – 检查表是否真的很大
- 确认热点路径 – 追踪调用点,验证这频繁运行
- 排除缓解措施 – 检查是否有缓存、限流
如果无法验证所有步骤,不要报告。
输出格式
## 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 原始英文文档,方便对照翻译。

评论(0)