Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
# {{ user.profile.bio }} โ triggers query per user
# {% endfor %}
# SOLUTION: Prefetch in view
def
user_list
request
'profile'
return
'users.html'
'users'
Rule: Prefetch in serializers, not just views
DRF serializers accessing related fields cause N+1 if queryset isn't optimized.
# PROBLEM: SerializerMethodField queries per objectclassUserSerializer(serializers.ModelSerializer):
order_count = serializers.SerializerMethodField()
defget_order_count(self, obj):
return obj.orders.count() # โ query per user# SOLUTION: Annotate in viewset, access in serializerclassUserViewSet(viewsets.ModelViewSet):
defget_queryset(self):
return User.objects.annotate(order_count=Count('orders'))
classUserSerializer(serializers.ModelSerializer):
order_count = serializers.IntegerField(read_only=True)
Rule: Model properties that query are dangerous in loops
# PROBLEM: Property triggers query when accessedclassUser(models.Model):
@propertydefrecent_orders(self):
returnself.orders.filter(created__gte=last_week)[:5]
# Used in template loop = N+1# SOLUTION: Use Prefetch with custom queryset, or annotate
Validation Checklist for N+1
Traced data flow from view to template/serializer
Confirmed related field is accessed inside a loop
Searched codebase for existing select_related/prefetch_related
Verified table has significant row count (1000+)
Confirmed this is a hot path (not admin, not rare action)
Priority 2: Unbounded Querysets (CRITICAL)
Impact: Loading entire tables exhausts memory. Large tables cause OOM kills and worker restarts.
Rule: Always paginate list endpoints
# PROBLEM: No pagination - loads all rowsclassUserListView(ListView):
model = User
template_name = 'users.html'# SOLUTION: Add paginationclassUserListView(ListView):
model = User
template_name = 'users.html'
paginate_by = 25
Rule: Use iterator() for large batch processing
# PROBLEM: Loads all objects into memory at oncefor user in User.objects.all():
process(user)
# SOLUTION: Stream with iterator()for user in User.objects.iterator(chunk_size=1000):
process(user)
Rule: Never call list() on unbounded querysets
# PROBLEM: Forces full evaluation into memory
all_users = list(User.objects.all())
# SOLUTION: Keep as queryset, slice if needed
users = User.objects.all()[:100]
Validation Checklist for Unbounded Querysets
Table is large (10k+ rows) or will grow unbounded
No pagination class, paginate_by, or slicing
This runs on user-facing request (not background job with chunking)
Priority 3: Missing Indexes (HIGH)
Impact: Full table scans. Negligible on small tables, catastrophic on large ones.
Rule: Index fields used in WHERE clauses on large tables
# PROBLEM: Filtering on unindexed field# User.objects.filter(email=email) # full scan if no indexclassUser(models.Model):
email = models.EmailField() # โ no db_index# SOLUTION: Add indexclassUser(models.Model):
email = models.EmailField(db_index=True)
Rule: Index fields used in ORDER BY on large tables
# PROBLEM: Sorting requires full scan without index
Order.objects.order_by('-created')
# SOLUTION: Index the sort fieldclassOrder(models.Model):
created = models.DateTimeField(db_index=True)
Rule: Use composite indexes for common query patterns
classOrder(models.Model):
user = models.ForeignKey(User)
status = models.CharField(max_length=20)
created = models.DateTimeField()
classMeta:
indexes = [
models.Index(fields=['user', 'status']), # for filter(user=x, status=y)
models.Index(fields=['status', '-created']), # for filter(status=x).order_by('-created')
]
Validation Checklist for Missing Indexes
Table has 10k+ rows
Field is used in filter() or order_by() on hot path
Checked model - no db_index=True or Meta.indexes entry
Not a foreign key (already indexed automatically)
Priority 4: Write Loops (HIGH)
Impact: N database writes instead of 1. Lock contention. Slow requests.
Rule: Use bulk_create instead of create() in loops
# PROBLEM: N inserts, N round tripsfor 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
])
Rule: Use update() or bulk_update instead of save() in loops
# PROBLEM: N updatesfor 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)
Rule: Use delete() on queryset, not in loops
# PROBLEM: N deletesfor obj in queryset:
obj.delete()
# SOLUTION: Single DELETE
queryset.delete()
Validation Checklist for Write Loops
Loop iterates over 100+ items (or unbounded)
Each iteration calls create(), save(), or delete()
This runs on user-facing request (not one-time migration script)
Priority 5: Inefficient Patterns (LOW)
Rarely worth reporting. Include only as minor notes if you're already reporting real issues.
# Fetches all rows to countiflen(queryset) > 0: # bad if queryset not yet evaluated# Single COUNT queryif queryset.count() > 0:
Only flag if queryset is large and not already evaluated.
Pattern: get() in small loops
# N queries, but if N is small (< 20), often fineforidin ids:
obj = Model.objects.get(id=id)
Only flag if loop is large or this is in a very hot path.
Validation Requirements
Before reporting ANY issue:
Trace the data flow - Follow queryset from creation to consumption
Search for existing optimizations - Grep for select_related, prefetch_related, pagination
Verify data volume - Check if table is actually large
Confirm hot path - Trace call sites, verify this runs frequently
Rule out mitigations - Check for caching, rate limiting
If you cannot validate all steps, do not report.
Output Format
## 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
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.