Django ORM: otimize as consultas para o máximo desempenho

Guia completo de otimização de consultas no ORM do Django. select_related, prefetch_related, índices, análise do problema N+1 e técnicas avançadas para aplicações de alto desempenho.

Otimização de consultas no ORM do Django para o máximo desempenho

O ORM do Django oferece uma abstração elegante para a interação com o banco de dados, mas essa simplicidade pode esconder problemas críticos de desempenho. Uma aplicação Django mal otimizada chega a gerar centenas de consultas onde uma única bastaria. Este guia explora as técnicas essenciais para identificar e resolver esses problemas.

A regra de ouro da otimização

Antes de otimizar, é preciso medir. O uso do django-debug-toolbar em desenvolvimento permite visualizar cada consulta SQL gerada e identificar rapidamente os gargalos.

Entender o problema N+1

O problema N+1 representa a armadilha mais comum em ORMs. Ele aparece quando uma consulta inicial recupera N objetos e, em seguida, são executadas N consultas adicionais para acessar as relações de cada objeto. Essa multiplicação de consultas degrada severamente o desempenho.

python
# models.py
from django.db import models

class Author(models.Model):
    """Modelo que representa o autor de um livro."""
    name = models.CharField(max_length=200)
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    """Modelo que representa um livro com seu autor."""
    title = models.CharField(max_length=300)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books'
    )
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)

    def __str__(self):
        return self.title

Esses modelos simples permitem ilustrar o problema N+1 de forma concreta.

python
# views.py - Exemplo problemático
def list_books_bad(request):
    """❌ Esta view gera N+1 consultas."""
    books = Book.objects.all()  # 1 consulta para os livros

    for book in books:
        # Cada acesso a book.author dispara uma consulta SQL
        print(f"{book.title} por {book.author.name}")

    # Com 100 livros = 101 consultas SQL!
    return render(request, 'books/list.html', {'books': books})

Esse código parece inofensivo, mas gera uma consulta por livro para recuperar o autor associado.

O método select_related realiza um JOIN em SQL e recupera os dados relacionados em uma única consulta. Funciona para relações ForeignKey e OneToOneField.

python
# views.py - Solução otimizada com select_related
def list_books_optimized(request):
    """✅ Esta view gera uma única consulta com JOIN."""
    # select_related realiza um JOIN SQL
    books = Book.objects.select_related('author').all()

    for book in books:
        # Nenhuma consulta adicional: o autor já está carregado
        print(f"{book.title} por {book.author.name}")

    # Total: 1 única consulta SQL, não importa o número de livros
    return render(request, 'books/list.html', {'books': books})

A consulta SQL gerada utiliza um LEFT OUTER JOIN para recuperar os autores junto com os livros.

python
# Encadear select_related para relações aninhadas
# models.py
class Publisher(models.Model):
    name = models.CharField(max_length=200)
    country = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)

# views.py
def list_books_with_details(request):
    """Recupera livros, autores e editoras em uma única consulta."""
    books = Book.objects.select_related(
        'author',      # ForeignKey para Author
        'publisher'    # ForeignKey para Publisher
    ).all()

    return render(request, 'books/list.html', {'books': books})

Várias relações podem ser otimizadas ao mesmo tempo listando-as em select_related.

Para relações ManyToMany ou relações reversas (ForeignKey vista do outro lado), o prefetch_related executa consultas separadas, porém otimizadas, evitando JOINs gigantes.

python
# models.py
class Tag(models.Model):
    """Tags para categorizar livros."""
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag, related_name='books')

A relação ManyToMany entre Book e Tag exige prefetch_related para uma otimização eficaz.

python
# views.py - Otimização ManyToMany
def list_books_with_tags(request):
    """✅ Recupera os livros e suas tags com eficiência."""
    books = Book.objects.prefetch_related('tags').all()

    for book in books:
        # As tags são pré-carregadas, sem consulta adicional
        tag_names = [tag.name for tag in book.tags.all()]
        print(f"{book.title}: {', '.join(tag_names)}")

    # Total: 2 consultas (livros + tags), independentemente do volume
    return render(request, 'books/list.html', {'books': books})

O prefetch_related executa uma consulta separada para as tags e depois faz a junção em Python.

select_related vs prefetch_related

Utilize select_related para ForeignKey e OneToOne (JOIN SQL). Utilize prefetch_related para ManyToMany e relações reversas (consultas separadas). Ambos podem ser combinados em um mesmo QuerySet.

Personalizar o pré-carregamento com objetos Prefetch

O objeto Prefetch permite controle fino sobre os dados pré-carregados: filtragem, ordenação e até limitação do número de resultados.

python
# views.py
from django.db.models import Prefetch

def list_authors_with_recent_books(request):
    """Recupera os autores apenas com seus livros recentes."""
    # Prefetch personalizado: somente livros a partir de 2025
    recent_books_prefetch = Prefetch(
        'books',
        queryset=Book.objects.filter(
            published_date__year__gte=2025
        ).order_by('-published_date'),
        to_attr='recent_books'  # Armazenado em um atributo personalizado
    )

    authors = Author.objects.prefetch_related(
        recent_books_prefetch
    ).all()

    for author in authors:
        # Acesso pelo atributo personalizado
        for book in author.recent_books:
            print(f"{author.name}: {book.title}")

    return render(request, 'authors/list.html', {'authors': authors})

O atributo to_attr guarda os resultados em uma lista Python em vez do manager habitual.

python
# Combinação avançada: select_related + Prefetch
def list_authors_complete(request):
    """Exemplo completo de otimização em vários níveis."""
    authors = Author.objects.prefetch_related(
        Prefetch(
            'books',
            queryset=Book.objects.select_related(
                'publisher'  # Otimiza também a editora de cada livro
            ).prefetch_related(
                'tags'       # E as tags de cada livro
            ).filter(published_date__year=2026)
        )
    ).all()

    return render(request, 'authors/complete.html', {'authors': authors})

Essa abordagem reduz drasticamente o número de consultas em estruturas de dados complexas.

Pronto para mandar bem nas entrevistas de Django?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Usar only() e defer() para limitar as colunas

Por padrão, o Django recupera todas as colunas da tabela. Em modelos com muitos campos ou campos grandes, limitar as colunas melhora o desempenho.

python
# views.py
def list_books_minimal(request):
    """Recupera apenas as colunas necessárias."""
    # only() especifica as colunas a incluir
    books = Book.objects.only(
        'id',
        'title',
        'published_date'
    ).select_related('author')

    # Atenção: acessar um campo não incluído dispara uma consulta
    for book in books:
        print(book.title)  # OK, incluído
        # print(book.isbn)  # Geraria uma consulta adicional

    return render(request, 'books/list.html', {'books': books})

O método only() cria um objeto "deferred" que carrega somente as colunas indicadas.

python
# defer() para excluir colunas específicas
def list_authors_without_bio(request):
    """Exclui campos grandes raramente usados."""
    # defer() exclui as colunas indicadas
    authors = Author.objects.defer(
        'bio'  # O TextField não é carregado
    ).all()

    for author in authors:
        print(author.name)   # OK
        print(author.email)  # OK
        # author.bio carregaria o campo sob demanda

    return render(request, 'authors/list.html', {'authors': authors})

O defer() é o inverso do only(): as colunas listadas não são carregadas inicialmente.

Otimizar com values() e values_list()

Quando apenas alguns valores são necessários, sem objetos completos do modelo, values() e values_list() retornam dicionários ou tuplas mais leves.

python
# views.py
def get_book_titles(request):
    """Recupera apenas os títulos como uma lista."""
    # values_list retorna tuplas
    titles = Book.objects.values_list('title', flat=True)
    # Resultado: ['Livro 1', 'Livro 2', ...]

    # values retorna dicionários
    book_data = Book.objects.values('title', 'published_date')
    # Resultado: [{'title': 'Livro 1', 'published_date': ...}, ...]

    return render(request, 'books/titles.html', {'titles': titles})

Esses métodos evitam a instância de objetos do modelo, reduzindo o consumo de memória.

python
# Combinação com agregações
from django.db.models import Count, Avg

def get_author_statistics(request):
    """Estatísticas por autor sem carregar objetos completos."""
    stats = Author.objects.values('name').annotate(
        book_count=Count('books'),
        avg_year=Avg('books__published_date__year')
    ).order_by('-book_count')

    # Resultado: [{'name': 'Autor', 'book_count': 5, 'avg_year': 2024}, ...]
    return render(request, 'authors/stats.html', {'stats': stats})

A anotação permite executar cálculos diretamente no banco de dados.

Criar índices para acelerar as consultas

Índices no banco de dados aceleram drasticamente as buscas. O Django permite defini-los diretamente nos modelos.

python
# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=300, db_index=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)
    status = models.CharField(max_length=20, default='available')

    class Meta:
        # Índices compostos para consultas frequentes
        indexes = [
            # Índice na data de publicação (ordenação frequente)
            models.Index(
                fields=['published_date'],
                name='book_pub_date_idx'
            ),
            # Índice composto para filtrar por autor + status
            models.Index(
                fields=['author', 'status'],
                name='book_author_status_idx'
            ),
            # Índice parcial: apenas livros disponíveis
            models.Index(
                fields=['published_date'],
                name='book_available_idx',
                condition=models.Q(status='available')
            ),
        ]
        # Ordenação padrão que aproveita o índice
        ordering = ['-published_date']

Esses índices melhoram o desempenho das consultas que filtram por essas colunas.

Índices e operações de escrita

Índices aceleram leituras, mas tornam as escritas (INSERT, UPDATE) ligeiramente mais lentas. Crie índices apenas em colunas usadas com frequência em cláusulas WHERE, ORDER BY ou JOIN.

Recorrer a consultas SQL brutas quando necessário

Para consultas complexas ou otimizações específicas do banco, consultas SQL brutas oferecem controle total.

python
# views.py
from django.db import connection

def get_books_with_raw_sql(request):
    """Consulta bruta para casos especiais."""
    # Método 1: raw() para recuperar objetos do modelo
    books = Book.objects.raw('''
        SELECT b.*, a.name as author_name
        FROM library_book b
        INNER JOIN library_author a ON b.author_id = a.id
        WHERE b.published_date > %s
        ORDER BY b.published_date DESC
    ''', ['2025-01-01'])

    return render(request, 'books/list.html', {'books': books})

def execute_custom_query(request):
    """Execução direta para consultas que não são SELECT."""
    with connection.cursor() as cursor:
        # Consulta com agregação complexa
        cursor.execute('''
            SELECT
                a.name,
                COUNT(b.id) as book_count,
                AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year
            FROM library_author a
            LEFT JOIN library_book b ON b.author_id = a.id
            GROUP BY a.id, a.name
            HAVING COUNT(b.id) > 2
            ORDER BY book_count DESC
        ''')
        results = cursor.fetchall()

    return render(request, 'stats.html', {'results': results})

Consultas brutas dispensam o ORM, mas perdem a portabilidade entre bancos.

Analisar consultas com django-debug-toolbar

A ferramenta django-debug-toolbar permite visualizar todas as consultas SQL geradas por uma view do Django.

python
# settings.py - Configuração da debug toolbar
INSTALLED_APPS = [
    # ... outras aplicações
    'debug_toolbar',
]

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

# Mostrar a toolbar para requisições locais
INTERNAL_IPS = ['127.0.0.1']

DEBUG_TOOLBAR_PANELS = [
    'debug_toolbar.panels.sql.SQLPanel',      # Consultas SQL
    'debug_toolbar.panels.timer.TimerPanel',  # Tempo de execução
    'debug_toolbar.panels.cache.CachePanel',  # Cache
]

Essa configuração ativa os painéis mais úteis para a otimização.

python
# Logging das consultas SQL em desenvolvimento
# settings.py
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

Esse logging exibe cada consulta SQL no console, útil para identificar problemas N+1.

Cachear QuerySets

Para dados acessados com frequência e raramente alterados, o cache evita consultas repetitivas.

python
# views.py
from django.core.cache import cache
from django.views.decorators.cache import cache_page

def list_featured_books(request):
    """Recupera os livros em destaque com cache."""
    cache_key = 'featured_books_list'

    # Tentativa de recuperação no cache
    books = cache.get(cache_key)

    if books is None:
        # Cache miss: executar a consulta
        books = list(
            Book.objects.select_related('author')
            .filter(featured=True)
            .order_by('-published_date')[:10]
        )
        # Armazenar em cache por 5 minutos
        cache.set(cache_key, books, timeout=300)

    return render(request, 'books/featured.html', {'books': books})

# Decorador para cachear a view inteira
@cache_page(60 * 15)  # Cache de 15 minutos
def list_all_tags(request):
    """Lista todas as tags (dados raramente alterados)."""
    tags = Tag.objects.annotate(
        book_count=Count('books')
    ).order_by('-book_count')

    return render(request, 'tags/list.html', {'tags': tags})

O cache reduz a carga sobre o banco de dados em dados estáticos.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Conclusão

A otimização de consultas com o ORM do Django apoia-se em alguns princípios fundamentais:

Medir antes de otimizar: usar o django-debug-toolbar para identificar problemas reais

Eliminar o N+1: select_related para ForeignKey, prefetch_related para ManyToMany

Limitar os dados: only(), defer(), values() para carregar apenas o necessário

Indexar com critério: criar índices em colunas frequentemente filtradas ou ordenadas

Cache estratégico: dados pouco modificados se beneficiam do cache do Django

Consultas brutas: como último recurso para otimizações específicas do banco

Aplicadas com método, essas técnicas transformam uma aplicação Django lenta em um sistema eficiente, capaz de lidar com grandes volumes de dados. O ORM do Django continua sendo uma ferramenta poderosa quando suas nuances são dominadas.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#django
#django orm
#python
#otimização banco de dados
#desempenho

Compartilhar

Artigos relacionados