| name | django-performance |
| description | Analyze and optimize Django application performance |
| disable-model-invocation | true |
| allowed-tools | Read, Bash, Grep, Glob |
Django Performance Analysis & Optimization
Execute each section systematically to identify and fix performance issues.
1. Identify N+1 Queries
Find Missing select_related / prefetch_related
grep -rn "\.user\.\|\.author\.\|\.category\.\|\.profile\." --include="*.py" --include="*.html" .
grep -rn "objects\.\(filter\|all\|get\)" --include="*.py" . | grep -v "select_related\|prefetch_related"
Measure Query Count
@pytest.mark.django_db
def test_order_list_queries(django_assert_num_queries, authenticated_client):
OrderFactory.create_batch(20)
with django_assert_num_queries(2):
response = authenticated_client.get("/orders/")
assert response.status_code == 200
Fix Patterns
orders = Order.objects.all()
for order in orders:
print(order.user.email)
orders = Order.objects.select_related("user").all()
users = User.objects.prefetch_related("orders").all()
from django.db.models import Prefetch
orders = Order.objects.prefetch_related(
Prefetch(
"items",
queryset=OrderItem.objects.select_related("product"),
)
)
2. Database Index Analysis
Find Missing Indexes
grep -rn "\.filter(\|\.exclude(\|\.order_by(\|\.get(" --include="*.py" . | head -50
grep -rn "indexes\s*=" --include="*.py" */models*.py
Common Index Candidates
class Meta:
indexes = [
models.Index(fields=["status"]),
models.Index(fields=["user", "status", "-created"]),
models.Index(
fields=["status"],
name="%(app_label)s_%(class)s_active_idx",
condition=models.Q(is_active=True),
),
GinIndex(
fields=["search_vector"],
name="%(app_label)s_%(class)s_search_idx",
),
]
Verify Index Usage (PostgreSQL)
EXPLAIN ANALYZE SELECT * FROM orders_order WHERE status = 'active';
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY schemaname, relname;
3. QuerySet Optimization
Use Efficient QuerySet Methods
if len(Order.objects.filter(status="active")) > 0:
...
count = len(Order.objects.all())
if Order.objects.filter(status="active").exists():
...
count = Order.objects.count()
emails = [u.email for u in User.objects.all()]
emails = list(User.objects.values_list("email", flat=True))
for order in Order.objects.all():
process(order)
for order in Order.objects.all().iterator(chunk_size=1000):
process(order)
Bulk Operations
for item in items:
OrderItem.objects.create(**item)
OrderItem.objects.bulk_create([OrderItem(**item) for item in items], batch_size=1000)
for order in orders:
order.status = "archived"
order.save()
Order.objects.filter(id__in=order_ids).update(status="archived")
for order in orders:
order.status = compute_status(order)
Order.objects.bulk_update(orders, ["status"], batch_size=1000)
Database Annotations
orders = Order.objects.all()
for order in orders:
order.item_count = order.items.count()
from django.db.models import Count, Sum
orders = Order.objects.annotate(
item_count=Count("items"),
total_amount=Sum("items__price"),
)
4. Caching Strategy
View-Level Caching
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def product_list(request):
...
Template Fragment Caching
{% load cache %}
{% cache 900 sidebar request.user.id %}
{# Expensive sidebar content #}
{% endcache %}
Low-Level Caching
from django.core.cache import cache
def get_popular_products():
cache_key = "popular_products_v1"
products = cache.get(cache_key)
if products is None:
products = list(
Product.objects.annotate(order_count=Count("orderitem"))
.order_by("-order_count")[:10]
.values("id", "name", "order_count")
)
cache.set(cache_key, products, timeout=60 * 30)
return products
Cache Invalidation
from django.db.models.signals import post_save, post_delete
def invalidate_product_cache(sender, **kwargs):
cache.delete("popular_products_v1")
post_save.connect(invalidate_product_cache, sender=OrderItem)
post_delete.connect(invalidate_product_cache, sender=OrderItem)
5. Database Connection Pooling (Django 5.1+)
DATABASES = {
"default": {
...
"OPTIONS": {
"pool": True,
},
}
}
For high-traffic: consider PgBouncer for external connection pooling.
6. Async Opportunities
Identify I/O-Bound Views
grep -rn "requests\.\|httpx\.\|urllib" --include="*.py" */views*.py
grep -rn "objects\." --include="*.py" */views*.py | cut -d: -f1 | sort | uniq -c | sort -rn
Convert to Async
def dashboard(request):
weather = requests.get("https://api.weather.com/current").json()
orders = Order.objects.filter(user=request.user)
return render(request, "dashboard.html", {"weather": weather, "orders": orders})
import httpx
async def dashboard(request):
async with httpx.AsyncClient() as client:
weather_task = client.get("https://api.weather.com/current")
orders = [o async for o in Order.objects.filter(user=request.user)]
weather = (await weather_task).json()
return render(request, "dashboard.html", {"weather": weather, "orders": orders})
7. Static Files & Media
Whitenoise Configuration
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
...
]
CDN for Media Files
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
},
}
AWS_S3_CUSTOM_DOMAIN = "cdn.example.com"
8. Report Format
# Performance Analysis Report
## Query Analysis
- **Total queries on page load:** X
- **N+1 issues found:** Y
- **Missing indexes:** Z
## Findings
### [HIGH] N+1 Query in Order List View
**Location:** `orders/views.py:42`
**Queries:** 102 queries for 100 orders
**Fix:** Add `select_related("user")` to queryset
**Impact:** Reduces queries from 102 to 2
### [MEDIUM] Missing Index on Order.status
**Location:** `orders/models.py`
**Evidence:** Sequential scan on 500K rows
**Fix:** Add `models.Index(fields=["status", "-created"])`
**Impact:** Query time from 200ms to 5ms
## Recommendations
1. ...
2. ...