关于 django-perf-review
django-perf-review 是面向 Django 项目的代码审查技能,要求结合代码上下文验证性能问题后再报告。它重视证据和实际影响,也接受没有发现问题的审查结果。
主要功能
- 01追踪查询与数据流
- 02验证 N+1 查询风险
- 03检查无界 QuerySet 与分页
- 04评估索引和写入循环
- 05排除未经验证或影响较小的问题
适用场景
- 01审查 Django 视图和序列化器中的 N+1 查询
- 02检查列表接口是否无界加载数据
- 03调查常用路径中的缓慢写入
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ getsentry/ skills --skill django-perf-review安装后在 Claude Code 中输入/django-perf-review确认已加载。
附带文件LICENSE
详细介绍
Django 性能审查
审查 Django 代码,找出已验证的性能问题。先研究代码库,确认问题后再报告。只报告能够证实的问题。
审查方法
- 先研究——追踪数据流,检查是否已有优化,并核实数据量
- 报告前先验证——模式匹配不等于验证
- 没有发现也可以——不要为了显得全面而编造问题
- 严重程度必须与影响相符——如果你发现自己在 CRITICAL 级别的问题中写了“轻微”,那它就不是严重问题。请降低级别或跳过。
影响类别
按影响对问题进行分类。重点关注 CRITICAL 和 HIGH——这些问题会在规模增大时造成实际影响。
| 优先级 | 类别 | 影响 |
|---|---|---|
| 1 | N+1 查询 | CRITICAL——随数据量增加而成倍增长,导致超时 |
| 2 | 无界 QuerySet | CRITICAL——内存耗尽,进程因 OOM 被终止 |
| 3 | 缺少索引 | HIGH——大型数据表上会进行全表扫描 |
| 4 | 写入循环 | HIGH——锁竞争,请求变慢 |
| 5 | 低效模式 | LOW——很少值得报告 |
优先级 1:N+1 查询(CRITICAL)
影响: 每个 N+1 都会增加 O(n) 次数据库往返。100 行 = 多出 100 次查询。10,000 行 = 超时。
规则:预取循环中访问的关联数据
通过追踪以下流程进行验证:视图 → QuerySet → 模板/序列化器 → 循环访问
# PROBLEM: N+1 - each iteration queries profile
def user_list(request):
users = User.objects.all()
return render(request, 'users.html', {'users': users})
# Template:
# {% for user in users %}
# {{ user.profile.bio }} ← triggers query per user
# {% endfor %}
# SOLUTION: Prefetch in view
def user_list(request):
users = User.objects.select_related('profile')
return render(request, 'users.html', {'users': users})
规则:在序列化器中预取,而不只是视图中
如果 QuerySet 未优化,DRF 序列化器访问关联字段会导致 N+1。
# PROBLEM: SerializerMethodField queries per object
class UserSerializer(serializers.ModelSerializer):
order_count = serializers.SerializerMethodField()
def get_order_count(self, obj):
return obj.orders.count() # ← query per user
# SOLUTION: Annotate in viewset, access in serializer
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)
规则:会执行查询的模型属性在循环中很危险
# PROBLEM: Property triggers query when accessed
class User(models.Model):
@property
def recent_orders(self):
return self.orders.filter(created__gte=last_week)[:5]
# Used in template loop = N+1
# SOLUTION: Use Prefetch with custom queryset, or annotate
N+1 验证清单
- 已追踪从视图到模板/序列化器的数据流
- 已确认循环中访问了关联字段
- 已在代码库中搜索现有的 select_related/prefetch_related
- 已确认数据表行数较多(1000+)
- 已确认这是热点路径(不是管理后台,也不是罕见操作)
优先级 2:无界 QuerySet(CRITICAL)
影响: 加载整张数据表会耗尽内存。大型数据表会导致进程因 OOM 被终止并重启。
规则:始终对列表端点分页
# PROBLEM: No pagination - loads all rows
class UserListView(ListView):
model = User
template_name = 'users.html'
# SOLUTION: Add pagination
class UserListView(ListView):
model = User
template_name = 'users.html'
paginate_by = 25
规则:对大型批处理使用 iterator()
# PROBLEM: Loads all objects into memory at once
for user in User.objects.all():
process(user)
# SOLUTION: Stream with iterator()
for user in User.objects.iterator(chunk_size=1000):
process(user)
规则:绝不要对无界 QuerySet 调用 list()
# PROBLEM: Forces full evaluation into memory
all_users = list(User.objects.all())
# SOLUTION: Keep as queryset, slice if needed
users = User.objects.all()[:100]
无界 QuerySet 验证清单
- 数据表很大(10k+ 行)或会无界增长
- 没有分页类、paginate_by 或切片
- 这会在面向用户的请求中运行(不是使用分块处理的后台任务)
优先级 3:缺少索引(HIGH)
影响: 全表扫描。小型数据表上的影响可以忽略,大型数据表上则可能造成灾难性后果。
规则:为大型数据表 WHERE 子句中使用的字段建立索引
# PROBLEM: Filtering on unindexed field
# User.objects.filter(email=email) # full scan if no index
class User(models.Model):
email = models.EmailField() # ← no db_index
# SOLUTION: Add index
class User(models.Model):
email = models.EmailField(db_index=True)
规则:为大型数据表 ORDER BY 中使用的字段建立索引
# PROBLEM: Sorting requires full scan without index
Order.objects.order_by('-created')
# SOLUTION: Index the sort field
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']), # for filter(user=x, status=y)
models.Index(fields=['status', '-created']), # for filter(status=x).order_by('-created')
]
缺少索引验证清单
- 数据表有 10k+ 行
- 热点路径中的 filter() 或 order_by() 使用了该字段
- 已检查模型——没有 db_index=True 或 Meta.indexes 条目
- 该字段不是外键(外键已自动建立索引)
优先级 4:写入循环(HIGH)
影响: 进行 N 次数据库写入,而不是 1 次。造成锁竞争,使请求变慢。
规则:在循环中使用 bulk_create,而不是 create()
# PROBLEM: N inserts, N round trips
for item in items:
Model.objects.create(name=item['name'])
# SOLUTION: Single bulk insert
Model.objects.bulk_create([
Model(name=item['name']) for item in items
])
规则:在循环中使用 update() 或 bulk_update,而不是 save()
# PROBLEM: N updates
for obj in queryset:
obj.status = 'done'
obj.save()
# SOLUTION A: Single UPDATE statement (same value for all)
queryset.update(status='done')
# SOLUTION B: bulk_update (different values)
for obj in objects:
obj.status = compute_status(obj)
Model.objects.bulk_update(objects, ['status'], batch_size=500)
规则:对 QuerySet 调用 delete(),不要在循环中逐个删除
# PROBLEM: N deletes
for obj in queryset:
obj.delete()
# SOLUTION: Single DELETE
queryset.delete()
写入循环验证清单
- 循环迭代 100+ 个项目(或数量无界)
- 每次迭代都会调用 create()、save() 或 delete()
- 这会在面向用户的请求中运行(不是一次性的迁移脚本)
优先级 5:低效模式(LOW)
很少值得报告。 只有在已报告实际问题时,才将其作为次要备注。
模式:count() 与 exists() 的比较
# Slightly suboptimal
if queryset.count() > 0:
do_thing()
# Marginally better
if queryset.exists():
do_thing()
通常跳过——大多数情况下,差异不到 1 毫秒。
模式:len(queryset) 与 count() 的比较
# Fetches all rows to count
if len(queryset) > 0: # bad if queryset not yet evaluated
# Single COUNT query
if queryset.count() > 0:
仅当 QuerySet 很大且尚未求值时,才标记此问题。
模式:在小型循环中使用 get()
# N queries, but if N is small (< 20), often fine
for id in ids:
obj = Model.objects.get(id=id)
仅在循环较大或位于非常高频的路径中时才标记。
验证要求
报告任何问题前:
- 追踪数据流——跟踪 queryset 从创建到使用的过程
- 搜索现有优化——搜索 select_related、prefetch_related、分页
- 确认数据量——检查表是否确实很大
- 确认高频路径——追踪调用位置,确认此代码是否经常运行
- 排除缓解措施——检查是否有缓存、速率限制
如果无法验证所有步骤,请勿报告。
输出格式
## Django Performance Review: [File/Component Name]
### Summary
Validated issues: X (Y Critical, Z High)
### Findings
#### [PERF-001] N+1 Query in UserListView (CRITICAL)
**Location:** `views.py:45`
**Issue:** Related field `profile` accessed in template loop without prefetch.
**Validation:**
- Traced: UserListView → users queryset → user_list.html → `{{ user.profile.bio }}` in loop
- Searched codebase: no select_related('profile') found
- User table: 50k+ rows (verified in admin)
- Hot path: linked from homepage navigation
**Evidence:**
```python
def get_queryset(self):
return User.objects.filter(active=True) # no select_related
修复:
def get_queryset(self):
return User.objects.filter(active=True).select_related('profile')
If no issues found: "No performance issues identified after reviewing [files] and validating [what you checked]."
**Before submitting, sanity check each finding:**
- Does the severity match the actual impact? ("Minor inefficiency" ≠ CRITICAL)
- Is this a real performance issue or just a style preference?
- Would fixing this measurably improve performance?
If the answer to any is "no" - remove the finding.
---
## What NOT to Report
- Test files
- Admin-only views
- Management commands
- Migration files
- One-time scripts
- Code behind disabled feature flags
- Tables with <1000 rows that won't grow
- Patterns in cold paths (rarely executed code)
- Micro-optimizations (exists vs count, only/defer without evidence)
### False Positives to Avoid
**Queryset variable assignment is not an issue:**
```python
# This is FINE - no performance difference
projects_qs = Project.objects.filter(org=org)
projects = list(projects_qs)
# vs this - identical performance
projects = list(Project.objects.filter(org=org))
Queryset 是惰性的。将其赋值给变量不会执行任何操作。
单次查询模式不属于 N+1:
# This is ONE query, not N+1
projects = list(Project.objects.filter(org=org))
N+1 需要通过循环触发额外查询。一次 list() 调用没有问题。
在单个对象获取时缺少 select_related 不属于 N+1:
# This is 2 queries, not N+1 - report as LOW at most
state = AutofixState.objects.filter(pr_id=pr_id).first()
project_id = state.request.project_id # second query
N+1 需要循环。单个对象执行 2 次查询而非 1 次,如果相关,可报告为 LOW,但绝不能报告为 CRITICAL/HIGH。
风格偏好不属于性能问题: 如果你唯一的建议是“合并这两行”或“重命名这个变量”,那属于风格问题,而非性能问题。请勿报告。