django-perf-review by getsentry/skills
npx skills add https://github.com/getsentry/skills --skill django-perf-review审查 Django 代码中已验证的性能问题。在报告前,请先研究代码库以确认问题。只报告你能证明的问题。
问题按影响组织。重点关注 CRITICAL 和 HIGH 级别——这些会在规模扩大时导致实际问题。
| 优先级 | 类别 | 影响 |
|---|---|---|
| 1 | N+1 查询 | CRITICAL - 随数据量倍增,导致超时 |
| 2 | 无限制的查询集 | CRITICAL - 内存耗尽,OOM 终止 |
| 3 | 缺失索引 | HIGH - 对大表进行全表扫描 |
| 4 | 写入循环 | HIGH - 锁竞争,请求变慢 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 5 | 低效模式 | LOW - 通常不值得报告 |
影响: 每个 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,或进行注解
影响: 加载整个表会耗尽内存。大表会导致 OOM 终止和工作进程重启。
# 问题: 无分页 - 加载所有行
class UserListView(ListView):
model = User
template_name = 'users.html'
# 解决方案: 添加分页
class UserListView(ListView):
model = User
template_name = 'users.html'
paginate_by = 25
# 问题: 一次性将所有对象加载到内存中
for user in User.objects.all():
process(user)
# 解决方案: 使用 iterator() 流式处理
for user in User.objects.iterator(chunk_size=1000):
process(user)
# 问题: 强制完全求值到内存中
all_users = list(User.objects.all())
# 解决方案: 保持为查询集,如果需要则切片
users = User.objects.all()[:100]
影响: 全表扫描。对小表影响可忽略,对大表是灾难性的。
# 问题: 在未索引字段上过滤
# 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.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')
]
影响: N 次数据库写入而不是 1 次。锁竞争。请求变慢。
# 问题: N 次插入,N 次往返
for item in items:
Model.objects.create(name=item['name'])
# 解决方案: 单次批量插入
Model.objects.bulk_create([
Model(name=item['name']) for item in items
])
# 问题: 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)
# 问题: N 次删除
for obj in queryset:
obj.delete()
# 解决方案: 单次 DELETE
queryset.delete()
通常不值得报告。 只有在已经报告真实问题时,才作为次要说明包含。
# 略微次优
if queryset.count() > 0:
do_thing()
# 稍好一些
if queryset.exists():
do_thing()
通常跳过 - 在大多数情况下差异 <1ms。
# 获取所有行来计数
if len(queryset) > 0: # 如果查询集尚未求值则不好
# 单次 COUNT 查询
if queryset.count() > 0:
仅在查询集很大且尚未求值时才标记。
# N 次查询,但如果 N 很小 (< 20),通常没问题
for id in ids:
obj = Model.objects.get(id=id)
仅在循环很大或这在非常热门的路径中时才标记。
在报告任何问题之前:
如果你无法验证所有步骤,请不要报告。
## Django 性能审查: [文件/组件名称]
### 摘要
已验证问题: X (Y 个关键, Z 个高)
### 发现
#### [PERF-001] UserListView 中的 N+1 查询 (CRITICAL)
**位置:** `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')
如果未发现问题: "在审查 [文件] 并验证 [你检查的内容] 后,未发现性能问题。"
提交前,请对每个发现进行合理性检查:
如果任何答案是"否" - 请移除该发现。
查询集变量赋值不是问题:
# 这没问题 - 没有性能差异
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 - 最多报告为 LOW
state = AutofixState.objects.filter(pr_id=pr_id).first()
project_id = state.request.project_id # 第二次查询
N+1 需要一个循环。单个对象进行 2 次查询而不是 1 次,如果相关,可以报告为 LOW,但绝不能是 CRITICAL/HIGH。
风格偏好不是性能问题: 如果你的唯一建议是"合并这两行"或"重命名这个变量" - 那是风格问题,不是性能问题。不要报告它。
每周安装量
277
仓库
GitHub 星标
458
首次出现
2026年2月1日
安全审计
安装于
gemini-cli241
opencode239
claude-code237
codex237
github-copilot236
kimi-cli225
Review Django code for validated performance issues. Research the codebase to confirm issues before reporting. Report only what you can prove.
Issues are organized by impact. Focus on CRITICAL and HIGH - these cause real problems at scale.
| Priority | Category | Impact |
|---|---|---|
| 1 | N+1 Queries | CRITICAL - Multiplies with data, causes timeouts |
| 2 | Unbounded Querysets | CRITICAL - Memory exhaustion, OOM kills |
| 3 | Missing Indexes | HIGH - Full table scans on large tables |
| 4 | Write Loops | HIGH - Lock contention, slow requests |
| 5 | Inefficient Patterns | LOW - Rarely worth reporting |
Impact: Each N+1 adds O(n) database round trips. 100 rows = 100 extra queries. 10,000 rows = timeout.
Validate by tracing: View → Queryset → Template/Serializer → Loop access
# 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})
DRF serializers accessing related fields cause N+1 if queryset isn't optimized.
# 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
Impact: Loading entire tables exhausts memory. Large tables cause OOM kills and worker restarts.
# 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
# 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)
# PROBLEM: Forces full evaluation into memory
all_users = list(User.objects.all())
# SOLUTION: Keep as queryset, slice if needed
users = User.objects.all()[:100]
Impact: Full table scans. Negligible on small tables, catastrophic on large ones.
# 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)
# 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')
]
Impact: N database writes instead of 1. Lock contention. Slow requests.
# 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
])
# 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)
# PROBLEM: N deletes
for obj in queryset:
obj.delete()
# SOLUTION: Single DELETE
queryset.delete()
Rarely worth reporting. Include only as minor notes if you're already reporting real issues.
# Slightly suboptimal
if queryset.count() > 0:
do_thing()
# Marginally better
if queryset.exists():
do_thing()
Usually skip - difference is <1ms in most cases.
# Fetches all rows to count
if len(queryset) > 0: # bad if queryset not yet evaluated
# Single COUNT query
if queryset.count() > 0:
Only flag if queryset is large and not already evaluated.
# N queries, but if N is small (< 20), often fine
for id in ids:
obj = Model.objects.get(id=id)
Only flag if loop is large or this is in a very hot path.
Before reporting ANY issue:
If you cannot validate all steps, do not report.
## 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
Fix:
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))
Querysets are lazy. Assigning to a variable doesn't execute anything.
Single query patterns are not N+1:
# This is ONE query, not N+1
projects = list(Project.objects.filter(org=org))
N+1 requires a loop that triggers additional queries. A single list() call is fine.
Missing select_related on single object fetch is not 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 requires a loop. A single object doing 2 queries instead of 1 can be reported as LOW if relevant, but never as CRITICAL/HIGH.
Style preferences are not performance issues: If your only suggestion is "combine these two lines" or "rename this variable" - that's style, not performance. Don't report it.
Weekly Installs
277
Repository
GitHub Stars
458
First Seen
Feb 1, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli241
opencode239
claude-code237
codex237
github-copilot236
kimi-cli225
GSAP React 动画库使用指南:useGSAP Hook 与最佳实践
1,400 周安装
Django部署Google Cloud SQL PostgreSQL教程:10分钟快速配置与生产环境设置
272 周安装
代码复杂度分析工具:Python/Go代码质量检测与重构指南
273 周安装
批量处理器技能 - 高效批量处理文档,支持PDF转换、文本提取、文件重命名
273 周安装
Cypress 自动化测试指南:E2E 与组件测试最佳实践、安装配置与故障排除
273 周安装
Antigravity Manager - AI账户管理器与代理网关,支持Gemini/Claude多账户轮换与协议转换
273 周安装
Inngest 持久化函数教程:构建容错工作流与 TypeScript 实践指南
273 周安装