Introduction
Indexes are the single most impactful optimization for database read performance. Django's Meta.indexes lets you declaratively define indexes on your models, and migrations handle the rest.
Key Concepts
- Index: A database structure that speeds up lookups on specific columns at the cost of additional storage and slower writes.
- B-tree Index: The default index type — efficient for equality, range, and ordering queries.
- Composite Index: An index on multiple columns — the column order matters for query matching.
- Partial Index: An index with a
conditionthat only includes rows matching a filter.
Real World Context
A table with 10 million rows where status='active' is filtered on every request can go from 500ms to 2ms with the right index. Most Django performance issues in production trace back to missing indexes.
Deep Dive
Declaring Indexes
Declaring indexes in Meta tells Django to create database indexes during migration, which dramatically speeds up filtered queries:
pythonclass Article(models.Model): title = models.CharField(max_length=200) status = models.CharField(max_length=20) pub_date = models.DateTimeField() author = models.ForeignKey('Author', on_delete=models.CASCADE) class Meta: indexes = [ # Single-column index models.Index(fields=['status'], name='article_status_idx'), # Composite index (column order matters!) models.Index(fields=['status', '-pub_date'], name='article_status_date_idx'), # Partial index (PostgreSQL, SQLite 3.9+) models.Index( fields=['pub_date'], name='article_published_idx', condition=models.Q(status='published') ), ]
The example above illustrates the pattern in practice. Now let's look at the next approach.
How Composite Indexes Work
Declaring indexes in Meta tells Django to create database indexes during migration, which dramatically speeds up filtered queries:
python# This index: ['status', '-pub_date'] # USES the index (leftmost prefix): Article.objects.filter(status='published') Article.objects.filter(status='published').order_by('-pub_date') Article.objects.filter(status='published', pub_date__gte=cutoff) # DOES NOT use the index: Article.objects.filter(pub_date__gte=cutoff) # Skips 'status' Article.objects.order_by('pub_date') # Wrong column order
The example above illustrates the pattern in practice. Now let's look at the next approach.
Verifying Index Usage
Declaring indexes in Meta tells Django to create database indexes during migration, which dramatically speeds up filtered queries:
python# Use .explain() to check if your index is used qs = Article.objects.filter(status='published').order_by('-pub_date') print(qs.explain(verbose=True)) # Look for: Index Scan using article_status_date_idx
The example above illustrates the pattern in practice. Now let's look at the next approach.
Unique Indexes
Declaring indexes in Meta tells Django to create database indexes during migration, which dramatically speeds up filtered queries:
pythonclass UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) username_slug = models.SlugField() class Meta: constraints = [ models.UniqueConstraint( fields=['username_slug'], name='unique_username_slug' ), ]
Common Pitfalls
- Indexing every column — Each index slows down INSERT/UPDATE operations. Only index columns used in WHERE, ORDER BY, and JOIN clauses.
- Wrong composite index order —
['status', 'date']helpsfilter(status=X)but notfilter(date=Y). Put the most selective column first. - Ignoring partial indexes — A full index on a column where 90% of queries filter
status='active'wastes space. Use a conditional index.
Best Practices
- Profile before indexing — Use
.explain()anddjango-debug-toolbarto identify slow queries, then add targeted indexes. - Use partial indexes for filtered queries — If you always filter on
status='published', a partial index is smaller and faster. - Review indexes periodically — Remove unused indexes that slow down writes without benefiting reads.
Summary
- Define indexes in
Meta.indexes— Django handles migration generation. - Composite index column order determines which queries benefit.
- Partial indexes (with
condition) are smaller and faster for filtered queries. - Use
.explain()to verify index usage. - Balance read speed against write overhead — don't over-index.
Code Examples
class Article(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20)
pub_date = models.DateTimeField()
class Meta:
indexes = [
models.Index(fields=['status', '-pub_date'], name='status_date_idx'),
models.Index(
fields=['pub_date'],
name='published_date_idx',
condition=models.Q(status='published') # Partial index
),
]