Skip to content

Chapter 16: Performance Optimization

16.1 Database Optimization

Query Optimization

Database query optimization is key to improving Django application performance:

python
# models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['slug']),
        ]

class Tag(models.Model):
    name = models.CharField(max_length=50)
    slug = models.SlugField(unique=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['slug']),
        ]

class Article(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, db_index=True)
    tags = models.ManyToManyField(Tag, blank=True)
    content = models.TextField()
    view_count = models.PositiveIntegerField(default=0, db_index=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, default='draft', db_index=True)
    
    class Meta:
        indexes = [
            models.Index(fields=['status', 'created_at']),
            models.Index(fields=['author', 'created_at']),
            models.Index(fields=['view_count']),
            models.Index(fields=['-created_at']),
        ]
        ordering = ['-created_at']

# views.py - Query before optimization
def article_list_bad(request):
    """Poor performance article list query"""
    articles = Article.objects.all()  # Get all fields
    for article in articles:
        # N+1 query problem
        print(article.author.username)  # Query author each time
        print(article.category.name)    # Query category each time
        for tag in article.tags.all():  # Query tags each time
            print(tag.name)
    return render(request, 'article_list.html', {'articles': articles})

# views.py - Optimized query
def article_list_good(request):
    """Optimized article list query"""
    articles = Article.objects.select_related(
        'author', 'category'  # Preload foreign key related objects
    ).prefetch_related(
        'tags'  # Preload many-to-many relationships
    ).filter(
        status='published'
    ).only(  # Only get needed fields
        'title', 'content', 'author__username', 'category__name'
    )[:20]  # Limit result count
    
    return render(request, 'article_list.html', {'articles': articles})

# Using Prefetch for more precise control
from django.db.models import Prefetch

def article_list_advanced(request):
    """Advanced optimized article list query"""
    # Preload tags with filtering conditions
    articles = Article.objects.select_related(
        'author', 'category'
    ).prefetch_related(
        Prefetch(
            'tags',
            queryset=Tag.objects.filter(name__startswith='tech'),
            to_attr='tech_tags'  # Custom attribute name
        )
    ).filter(
        status='published'
    ).annotate(
        # Add annotation fields
        comment_count=models.Count('comments', distinct=True)
    ).order_by('-created_at')[:20]
    
    return render(request, 'article_list.html', {'articles': articles})

# Avoid unnecessary queries
def article_detail_optimized(request, slug):
    """Optimized article detail query"""
    try:
        # Use select_related and prefetch_related
        article = Article.objects.select_related(
            'author__profile', 'category'
        ).prefetch_related(
            Prefetch(
                'comments',
                queryset=Comment.objects.select_related('author').filter(
                    is_approved=True, parent=None
                ),
                to_attr='approved_comments'
            ),
            'tags'
        ).get(slug=slug, status='published')
        
        # Get related articles (avoid N+1 query)
        related_articles = Article.objects.filter(
            category=article.category,
            status='published'
        ).exclude(
            pk=article.pk
        ).select_related('author').only(
            'title', 'slug', 'created_at'
        )[:5]
        
        return render(request, 'article_detail.html', {
            'article': article,
            'related_articles': related_articles
        })
    except Article.DoesNotExist:
        raise Http404("Article not found")

# Use aggregate queries to reduce database access
def category_stats(request):
    """Category statistics query"""
    # Use annotate for aggregation
    categories = Category.objects.annotate(
        article_count=models.Count('article', filter=models.Q(article__status='published')),
        total_views=models.Sum('article__view_count', filter=models.Q(article__status='published'))
    ).filter(
        article_count__gt=0
    ).order_by('-article_count')
    
    return render(request, 'category_stats.html', {'categories': categories})

# Batch operation optimization
def bulk_create_articles(request):
    """Optimized bulk article creation"""
    articles_data = [
        {'title': f'Article{i}', 'content': f'Content{i}', 'author_id': 1}
        for i in range(1000)
    ]
    
    # Use bulk_create to reduce SQL query count
    articles = [Article(**data) for data in articles_data]
    Article.objects.bulk_create(articles, batch_size=100)
    
    return JsonResponse({'status': 'success'})

def bulk_update_articles(request):
    """Optimized bulk article update"""
    # Get articles that need to be updated
    articles = Article.objects.filter(status='draft')
    
    # Use bulk_update to reduce SQL query count
    for article in articles:
        article.status = 'published'
    
    Article.objects.bulk_update(articles, ['status'], batch_size=100)
    
    return JsonResponse({'status': 'success'})

Detailed usage of select_related and prefetch_related:

python
# select_related example - for foreign key and one-to-one relationships
def select_related_examples():
    """select_related usage examples"""
    
    # Basic usage
    articles = Article.objects.select_related('author').all()
    # Generated SQL: SELECT * FROM article INNER JOIN auth_user ON ...
    
    # Multi-level relationships
    articles = Article.objects.select_related('author__profile', 'category').all()
    # Generated SQL: SELECT * FROM article 
    #            INNER JOIN auth_user ON ... 
    #            INNER JOIN user_profile ON ...
    #            INNER JOIN category ON ...
    
    # Combined with filtering
    published_articles = Article.objects.select_related(
        'author', 'category'
    ).filter(
        status='published'
    ).order_by('-created_at')[:10]

# prefetch_related example - for many-to-many and reverse foreign key relationships
def prefetch_related_examples():
    """prefetch_related usage examples"""
    
    # Basic usage
    articles = Article.objects.prefetch_related('tags').all()
    # Generate two SQL queries: 
    # 1. SELECT * FROM article
    # 2. SELECT * FROM tag INNER JOIN article_tags ON ...
    
    # Multiple relationships
    articles = Article.objects.prefetch_related('tags', 'comments').all()
    
    # Nested preloading
    articles = Article.objects.prefetch_related(
        'tags',
        'comments__author'  # Author of comments
    ).all()
    
    # Using Prefetch object for fine control
    from django.db.models import Prefetch
    
    articles = Article.objects.prefetch_related(
        Prefetch(
            'comments',
            queryset=Comment.objects.select_related('author').filter(is_approved=True),
            to_attr='approved_comments'  # Custom attribute name
        )
    ).all()

# Performance comparison example
def performance_comparison():
    """Performance comparison example"""
    
    # Unoptimized query - N+1 problem
    print("Unoptimized query:")
    start_time = time.time()
    articles = Article.objects.all()
    for article in articles[:10]:  # Only look at first 10 articles
        print(f"Article: {article.title}")
        print(f"Author: {article.author.username}")  # Query database each time
        print(f"Category: {article.category.name}")    # Query database each time
        for tag in article.tags.all():            # Query database each time
            print(f"Tag: {tag.name}")
    end_time = time.time()
    print(f"Time: {end_time - start_time:.2f} seconds")
    
    # Optimized query
    print("\nOptimized query:")
    start_time = time.time()
    articles = Article.objects.select_related(
        'author', 'category'
    ).prefetch_related('tags').all()[:10]
    
    for article in articles:
        print(f"Article: {article.title}")
        print(f"Author: {article.author.username}")
        print(f"Category: {article.category.name}")
        for tag in article.tags.all():
            print(f"Tag: {tag.name}")
    end_time = time.time()
    print(f"Time: {end_time - start_time:.2f} seconds")

Database Indexes

Creating and optimizing database indexes:

python
# models.py
class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    content = models.TextField()
    view_count = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, default='draft')
    is_featured = models.BooleanField(default=False)
    
    class Meta:
        # Single field indexes
        indexes = [
            models.Index(fields=['slug']),  # Unique index already specified in field definition
            models.Index(fields=['view_count']),
            models.Index(fields=['created_at']),
            models.Index(fields=['status']),
            models.Index(fields=['is_featured']),
        ]
        
        # Composite indexes
        indexes += [
            models.Index(fields=['status', 'created_at']),  # Status and creation time
            models.Index(fields=['author', 'created_at']),  # Author and creation time
            models.Index(fields=['category', 'status', 'created_at']),  # Category, status, and creation time
        ]
        
        # Partial indexes (PostgreSQL)
        # indexes += [
        #     models.Index(
        #         fields=['created_at'],
        #         condition=models.Q(status='published'),
        #         name='published_articles_created_at_idx'
        #     ),
        # ]

# Custom index names
class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    
    class Meta:
        indexes = [
            models.Index(
                fields=['slug'],
                name='category_slug_idx'  # Custom index name
            ),
        ]

# Function indexes example (PostgreSQL)
# class Article(models.Model):
#     title = models.CharField(max_length=200)
#     
#     class Meta:
#         indexes = [
#             models.Index(
#                 models.functions.Lower('title'),
#                 name='article_title_lower_idx'
#             ),
#         ]

# Database query analysis
def analyze_queries():
    """Analyze database queries"""
    from django.db import connection
    from django.conf import settings
    
    # Enable query log
    settings.DEBUG = True
    
    # Execute query
    articles = Article.objects.select_related('author', 'category').filter(
        status='published'
    ).order_by('-created_at')[:20]
    
    # Analyze query
    print(f"Executed {len(connection.queries)} queries")
    for i, query in enumerate(connection.queries):
        print(f"Query {i+1}: {query['sql']}")
        print(f"Time: {query['time']} seconds")
        print("-" * 50)
    
    # Reset query log
    connection.queries_log.clear()

# Using django-debug-toolbar to monitor queries
# Install: pip install django-debug-toolbar

# settings.py
# INSTALLED_APPS = [
#     # ...
#     'debug_toolbar',
# ]

# MIDDLEWARE = [
#     # ...
#     'debug_toolbar.middleware.DebugToolbarMiddleware',
# ]

# INTERNAL_IPS = [
#     '127.0.0.1',
# ]

Through these database optimization techniques, you can significantly improve Django application query performance and reduce database load.

Summary

Core points of Django database optimization:

  1. ✅ Use select_related and prefetch_related to solve N+1 query problems
  2. ✅ Design database indexes reasonably to improve query efficiency
  3. ✅ Implement batch operations to reduce database access frequency
  4. ✅ Use aggregate queries and annotations to reduce data transmission
  5. ✅ Monitor and analyze query performance to identify bottlenecks

Database optimization is an important means to improve application performance.

Next Article

We will learn about using the caching system.

16.2 Cache System →

Directory

Return to Course Directory

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).