Imagine your Django site on PostgreSQL slowing down on every page. N+1 queries multiply, indexes are missing, connection pool is not configured — TTFB exceeds 2 seconds, Core Web Vitals fail, users leave. It hurts especially on catalog pages with thousands of products: one category query spawns hundreds of subqueries. Without proper ORM architecture, the project becomes a mess of queries, and the cost of each improvement rises.
We, certified engineers with 10 years of experience, configure Django ORM turnkey. We guarantee a measurable performance improvement: database response time drops up to 10 times, LCP falls by 60%, and maintenance costs are halved. Significant savings on cloud resources — a real case from one of our clients with a 50,000 product catalog saved $2000/month. Get an improvement plan in 1 day — just write to us.
Main Problems We Solve
- Django ORM performance optimization eliminates N+1 queries: instead of 1+N queries we make 2 (select_related/prefetch_related). This is 10-50 times more efficient than manual querying.
- Missing indexes: add composite indexes for frequent filters.
- At least one inefficient query per page — annotations and aggregations minimize them.
- No persistent connections — each connection opens anew, increasing latency 5 times. Persistent connections fix this, reducing delay up to 5 times compared to opening a new connection per request.
- No replication — master DB is overloaded. We set up automatic read routing to a replica, unloading the master.
Configuring PostgreSQL Connection
In settings.py we define multiple databases if needed. Persistent connections reduce latency up to 5 times. Settings table:
| Parameter | default | replica |
|---|---|---|
| ENGINE | django.db.backends.postgresql | django.db.backends.postgresql |
| CONN_MAX_AGE | 60 | 60 |
| connect_timeout | 10 | — |
| TEST mirror | — | default |
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST', default='127.0.0.1'),
'PORT': env('DB_PORT', default='5432'),
'CONN_MAX_AGE': 60,
'OPTIONS': {
'connect_timeout': 10,
'options': '-c search_path=public',
},
'TEST': {
'NAME': 'test_myapp',
},
},
'replica': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DB_REPLICA_NAME'),
'USER': env('DB_REPLICA_USER'),
'PASSWORD': env('DB_REPLICA_PASSWORD'),
'HOST': env('DB_REPLICA_HOST'),
'PORT': '5432',
'CONN_MAX_AGE': 60,
'TEST': {
'MIRROR': 'default',
},
},
}
Why Custom Managers Solve N+1?
Custom manager is the main tool for encapsulating query logic. It automatically loads related objects, eliminating N+1 queries. Compared to manually calling select_related in every view, a custom manager reduces queries by 10-50 times and centralizes logic.
Implementation steps:
- Define a QuerySet with methods for filtering and eager loading.
- Create a manager returning this QuerySet.
- Use the manager in code — method chains are readable and maintainable.
Example for a catalog: models Category and Product. Here is the model code:
from django.db import models
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, max_length=220)
parent = models.ForeignKey(
'self',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='children',
)
class Meta:
verbose_name_plural = 'categories'
ordering = ['name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Product(models.Model):
class Status(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
ARCHIVED = 'archived', 'Archived'
title = models.CharField(max_length=500)
slug = models.SlugField(unique=True, max_length=520)
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name='products',
)
price = models.DecimalField(max_digits=12, decimal_places=2)
status = models.CharField(
max_length=10,
choices=Status.choices,
default=Status.DRAFT,
)
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
indexes = [
models.Index(fields=['status', '-created_at']),
models.Index(fields=['category', 'status']),
]
class PublishedProductQuerySet(models.QuerySet):
def published(self):
return self.filter(status=Product.Status.PUBLISHED)
def with_category(self):
return self.select_related('category')
def with_tags(self):
return self.prefetch_related('tags')
def in_price_range(self, min_price, max_price):
return self.filter(price__gte=min_price, price__lte=max_price)
class ProductManager(models.Manager):
def get_queryset(self):
return PublishedProductQuerySet(self.model, using=self._db)
def published(self):
return self.get_queryset().published()
# Usage:
products = (
Product.objects.published()
.with_category()
.with_tags()
.in_price_range(100, 5000)
.order_by('-created_at')[:20]
)
How custom managers eliminate N+1 queries step by step
- QuerySet methods chain to create a single optimized query.
-
select_relatedjoins ForeignKey tables in one SQL. -
prefetch_relatedreduces ManyToMany lookups to 2 queries. - The manager ensures every view uses these methods by default.
Optimizing Queries with Annotations
Annotations and F-expressions allow aggregation and update in one query without Python round-trip:
from django.db.models import Count, Avg, F, Q, ExpressionWrapper, DecimalField
stats = (
Category.objects
.annotate(
product_count=Count('products', filter=Q(products__status='published')),
avg_price=Avg('products__price', filter=Q(products__status='published')),
)
.filter(product_count__gt=0)
.order_by('-product_count')
)
Product.objects.filter(status='published').update(
price=ExpressionWrapper(F('price') * 1.1, output_field=DecimalField())
)
This approach reduces the number of queries from dozens to one, directly affecting TTFB.
How to Set Up Replication with Automatic Routing?
Database replication reads from a replica, writes go to master. This unloads the primary DB and increases fault tolerance. Here's a simple router:
class ReadReplicaRouter:
READ_DB = 'replica'
WRITE_DB = 'default'
def db_for_read(self, model, **hints):
return self.READ_DB
def db_for_write(self, model, **hints):
return self.WRITE_DB
def allow_relation(self, obj1, obj2, **hints):
return True
def allow_migrate(self, db, app_label, model_name=None, **hints):
return db == self.WRITE_DB
# settings.py
DATABASE_ROUTERS = ['myapp.db_router.ReadReplicaRouter']
Setup steps:
- Create a PostgreSQL replica (physical or logical).
- Define databases in
DATABASES. - Implement the router as above.
- Enable the router in
DATABASE_ROUTERS.
Migration Rules in Production
- Adding a nullable column does not lock the table in PostgreSQL 11+.
- Create indexes using
CONCURRENTLY— Django uses it automatically for PostgreSQL, which does not lock the table during index creation. This is important for production. - Renaming a column: in two stages (add new → copy data → remove old).
-
--fake— only for state synchronization without re-running SQL.
What Is Included in Turnkey Django ORM Setup?
| Stage | Description | Duration |
|---|---|---|
| Audit current schema | Identify N+1, duplicate queries, missing indexes | 1 day |
| Design | Define indexing strategy, replication, managers | 0.5 day |
| Implementation | Configure connections, models, managers, router | 1–2 days |
| Testing | Load testing, performance verification | 0.5 day |
| Documentation and training | ER diagram, QuerySet description, recommendations | 0.5 day |
Deliverables:
- Database access credentials (read/write users, replica endpoints)
- Fully commented ER diagram (entity-relationship model)
- Trained team on QuerySet usage and replication principles
- 2 weeks of post-deployment support (Slack/email)
To improve Django ORM performance, contact us to assess your project and propose an action plan. Get a consultation on Django optimization — we will answer all questions. Start by ordering an audit of your current schema: we will analyze queries, indexes, and connections, then provide a detailed report with recommendations.







