5 Problemas de Performance no Django ORM e Como Resolver

Como desenvolvedores Django, frequentemente focamos em fazer nossas aplicações funcionarem corretamente. Escrevemos código limpo, seguimos boas práticas e testamos cuidadosamente. Mas há um assassino silencioso que pode derrubar até o código mais elegante: a performance do banco de dados.

Neste guia completo, vou guiar você pelos 5 problemas mais comuns de performance no Django ORM. Para cada problema, explicarei:

Vamos lá!


Os 5 Problemas de Performance de Um Olhar

#ProblemaSoluçãoQueries (Lento)Queries (Rápido)Impacto
1N+1 Query (ForeignKey)select_related()N+11🔴 Crítico
2N+1 Query (ManyToMany)prefetch_related()N+12🔴 Crítico
3SELECT * desnecessárioonly() / defer()11🟡 Médio
4len() vs count().count()12🟡 Médio
5Paginação ausentePaginatorTodosTamanho da página🔴 Crítico

Problema 1: N+1 Query com ForeignKey

🔍 O Que É?

O problema N+1 é o mais comum e mais danoso problema de performance em aplicações Django. Ele ocorre quando você busca uma lista de objetos e depois acessa um relacionamento ForeignKey para cada um deles.

🧠 Por Que Acontece?

Quando você chama Book.objects.all(), o Django executa:

SQL
SELECT * FROM book;
Clique para expandir e ver mais

Isso retorna 50 livros. Mas aqui está o ponto crucial: o Django é preguiçoso (lazy). Ele não carrega os dados relacionados author até que você realmente acesse-os.

Quando seu template ou código faz:

HTML
{% for livro in livros %}
    {{ livro.autor.nome }}  <!-- Acessando ForeignKey aqui -->
{% endfor %}
Clique para expandir e ver mais

O Django pensa: “Ah, você precisa do autor agora? Deixa eu buscar.” Então ele executa:

SQL
SELECT * FROM autor WHERE id = 1;
SELECT * FROM autor WHERE id = 2;
SELECT * FROM autor WHERE id = 3;
-- ... e assim por diante para cada livro!
Clique para expandir e ver mais

📊 A Matemática

LivrosQueries SQL (Lento)Queries SQL (Rápido)
10111
50511
1001011
1.0001.0011
10.00010.0011

Com apenas 1.000 livros, você está fazendo 1.001 viagens ao banco de dados! Cada viagem tem latência (tipicamente 1-10ms), então você está desperdiçando 1-10 segundos só em comunicação com o banco de dados.

💻 O Código

❌ Versão Lenta (O Problema):

PYTHON
# views.py
def livros_lento(request):
    """Isso dispara 51 queries!"""
    livros = Book.objects.all()[:50]  # Query 1: SELECT * FROM book
    for livro in livros:
        print(livro.autor.nome)  # Queries 2-51: Uma por livro!
    return render(request, 'livros.html', {'livros': livros})
Clique para expandir e ver mais

SQL Gerado:

SQL
-- Query 1
SELECT * FROM book LIMIT 50;

-- Queries 2-51 (uma para cada livro!)
SELECT * FROM autor WHERE id = 1;
SELECT * FROM autor WHERE id = 2;
...
SELECT * FROM autor WHERE id = 50;
Clique para expandir e ver mais

✅ Versão Rápida (A Solução):

PYTHON
# views.py
def livros_rapido(request):
    """Isso dispara apenas 1 query!"""
    livros = Book.objects.select_related('autor')[:50]
    for livro in livros:
        print(livro.autor.nome)  # Já está na memória!
    return render(request, 'livros.html', {'livros': livros})
Clique para expandir e ver mais

SQL Gerado:

SQL
-- Uma única query com JOIN!
SELECT livro.*, autor.*
FROM book AS livro
INNER JOIN autor ON livro.autor_id = autor.id
LIMIT 50;
Clique para expandir e ver mais

O método select_related() executa um JOIN SQL no nível do banco de dados. Os dados relacionados voltam em uma única query.

PYTHON
# Sintaxe
Book.objects.select_related('autor')              # 1 nível
Book.objects.select_related('autor', 'editora')  # Múltiplas relações
Book.objects.select_related('autor__pais')        # Aninhado (2 níveis)
Clique para expandir e ver mais

🧪 Exemplo Real em Template

❌ Lento:

HTML
<!-- templates/livros_lento.html -->
<h1>Livros</h1>
<table>
    <tr>
        <th>Título</th>
        <th>Autor</th>
        <th>Preço</th>
    </tr>
    {% for livro in livros %}
    <tr>
        <td>{{ livro.titulo }}</td>
        <td>{{ livro.autor.nome }}</td>  <!-- N+1 acontece aqui! -->
        <td>{{ livro.preco }}</td>
    </tr>
    {% endfor %}
</table>
Clique para expandir e ver mais

✅ Rápido:

HTML
<!-- templates/livros_rapido.html -->
<!-- Basta adicionar select_related('autor') na view! -->
Clique para expandir e ver mais

📝 Quando Usar


Problema 2: N+1 Query com ManyToMany

🔍 O Que É?

Similar ao problema de ForeignKey, mas ocorre com relacionamentos ManyToManyField e ForeignKey Reverso. Cada acesso a .all() em um ManyToMany dispara uma nova query.

🧠 Por Que Acontece?

Relacionamentos ManyToMany requerem uma tabela de junção no banco de dados. O Django não pode simplesmente buscar os itens relacionados em um JOIN simples como ForeignKey.

Código lento:

PYTHON
livros = Book.objects.all()[:50]  # Query 1: Buscar livros
for livro in livros:
    for tag in livro.tags.all():   # Queries 2-51: Buscar tags para cada livro!
        print(tag.nome)
Clique para expandir e ver mais

💻 O Código

❌ Versão Lenta:

PYTHON
def tags_lento(request):
    """Isso dispara 51 queries!"""
    livros = Book.objects.all()[:50]  # 1 query para livros
    for livro in livros:
        tags = livro.tags.all()  # 1 query por livro = 50 queries
        list(tags)
    return render(request, 'tags.html', {'livros': livros})
Clique para expandir e ver mais

SQL Gerado:

SQL
-- Query 1
SELECT * FROM book LIMIT 50;

-- Queries 2-51
SELECT tag.*
FROM tag
INNER JOIN livro_tags ON tag.id = livro_tags.tag_id
WHERE livro_tags.book_id = 1;

-- (repetido para cada livro)
Clique para expandir e ver mais

✅ Versão Rápida:

PYTHON
def tags_rapido(request):
    """Isso dispara apenas 2 queries!"""
    livros = Book.objects.prefetch_related('tags')[:50]
    for livro in livros:
        tags = livro.tags.all()  # Usa o cache pré-buscado
        list(tags)
    return render(request, 'tags.html', {'livros': livros})
Clique para expandir e ver mais

SQL Gerado:

SQL
-- Query 1: Buscar livros
SELECT * FROM book LIMIT 50;

-- Query 2: Buscar TODAS as tags relacionadas de uma vez
SELECT tag.*, livro_tags.book_id
FROM tag
INNER JOIN livro_tags ON tag.id = livro_tags.tag_id
WHERE livro_tags.book_id IN (1, 2, 3, ..., 50);
Clique para expandir e ver mais

Diferente de select_related() que usa um JOIN, prefetch_related() executa duas queries separadas e junta elas em Python. Isso é mais flexível e funciona com ManyToMany.

PYTHON
# Sintaxe
Book.objects.prefetch_related('tags')              # 1 nível
Book.objects.prefetch_related('tags', 'avaliacoes')  # Múltiplas
Book.objects.prefetch_related('tags__categoria')    # Aninhado
Clique para expandir e ver mais
Recursoselect_related()prefetch_related()
Operação SQLJOINDuas queries + junção Python
Queries12
Uso paraForeignKey, OneToOneManyToMany, FK Reverso
Pode filtrar relacionado❌ Limitado✅ Sim (com Prefetch)
Suporte aninhado✅ Sim✅ Sim

📝 Quando Usar Cada Um

PYTHON
# ForeignKey → select_related
Book.objects.select_related('autor')

# ManyToMany → prefetch_related
Book.objects.prefetch_related('tags')

# Ambos na mesma query
Book.objects.select_related('autor').prefetch_related('tags')

# ForeignKey Reverso → prefetch_related
Autor.objects.prefetch_related('livros')
Clique para expandir e ver mais

Problema 3: SELECT * Desnecessário

🔍 O Que É?

O método all() do Django gera SELECT *, que recupera todas as colunas da tabela do banco de dados, incluindo campos de texto grandes e dados binários que você não precisa.

🧠 Por Que Importa?

Considere este cenário:

PYTHON
class Livro(models.Model):
    titulo = models.CharField(max_length=200)       # ~200 bytes
    sinopse = models.TextField()                      # Pode ter 10KB+!
    imagem_capa = models.ImageField(upload_to='capas')  # Pode ter 1MB+!
    arquivo_pdf = models.FileField(upload_to='pdfs')  # Pode ter 10MB+!
    ano_publicacao = models.IntegerField()
    preco = models.DecimalField(max_digits=6, decimal_places=2)
Clique para expandir e ver mais

Se você só precisa de titulo e preco, mas faz query Livro.objects.all(), você está buscando 10MB+ de dados por livro desnecessariamente!

💻 O Código

❌ Versão Lenta:

PYTHON
def campos_lento(request):
    """Carrega TODOS os campos incluindo os grandes!"""
    livros = Livro.objects.all()[:50]
    dados = [{'titulo': l.titulo, 'preco': l.preco} for l in livros]
    return render(request, 'campos.html', {'dados': dados})
Clique para expandir e ver mais

SQL Gerado:

SQL
SELECT id, titulo, sinopse, imagem_capa, arquivo_pdf, ano_publicacao, preco
FROM livro
LIMIT 50;
Clique para expandir e ver mais

Isso busca todas as colunas, incluindo campos potencialmente massivos como sinopse, imagem_capa e arquivo_pdf.

✅ Versão Rápida:

PYTHON
def campos_rapido(request):
    """Carrega SOMENTE os campos que precisamos!"""
    livros = Livro.objects.only('titulo', 'preco')[:50]
    dados = [{'titulo': l.titulo, 'preco': l.preco} for l in livros]
    return render(request, 'campos.html', {'dados': dados})
Clique para expandir e ver mais

SQL Gerado:

SQL
SELECT id, titulo, preco
FROM livro
LIMIT 50;
Clique para expandir e ver mais

🎯 A Solução: only() e defer()

PYTHON
# only(): Carrega SOMENTE estes campos (mais id)
Livro.objects.only('titulo', 'preco')

# defer(): Carrega tudo EXCETO estes campos
Livro.objects.defer('sinopse', 'imagem_capa', 'arquivo_pdf')

# Relacionamentos aninhados
Livro.objects.select_related('autor').only('titulo', 'autor__nome')
Clique para expandir e ver mais

⚠️ Aviso Importante

Após usar only() ou defer(), o objeto está incompleto. Acessar um campo adiado dispara uma nova query!

PYTHON
livros = Livro.objects.only('titulo', 'preco')

livro = livros[0]
print(livro.titulo)     # ✅ OK - já carregado
print(livro.preco)      # ✅ OK - já carregado
print(livro.sinopse)    # ❌ NOVA QUERY! - campo adiado
Clique para expandir e ver mais

📊 Quando Usar

SituaçãoMétodo
Sabe exatamente quais campos precisaonly('campo1', 'campo2')
Sabe quais campos NÃO precisadefer('campo_grande', 'campo_arquivo')
Use com select_related()only('titulo', 'autor__nome')

Problema 4: len() vs count()

🔍 O Que É?

Usar len() em um QuerySet avalia todo o queryset e carrega todos os registros na memória só para contá-los. Usar .count() executa um SELECT COUNT(*) no nível do banco de dados.

🧠 Por Que Importa?

PYTHON
# Esses parecem similares mas são MUITO diferentes:
len(Livro.objects.all())      # Carrega TODOS 1 milhão de livros na memória!
Livro.objects.count()         # Apenas pergunta: "quantas linhas?"
Clique para expandir e ver mais

💻 O Código

❌ Versão Lenta:

PYTHON
def contagem_lenta(request):
    """Carrega TODOS os livros na memória só para contá-los!"""
    livros = Livro.objects.all()      # Carrega 1 milhão de registros
    total = len(livros)                # Python conta na memória
    return render(request, 'contagem.html', {'total': total})
Clique para expandir e ver mais

SQL Gerado:

SQL
SELECT * FROM livro;  # Retorna 1 milhão de linhas!
Clique para expandir e ver mais

Depois o Python conta na memória. Isso é extremamente lento e intensivo em memória.

✅ Versão Rápida:

PYTHON
def contagem_rapida(request):
    """Conta no nível do banco de dados - super rápido!"""
    total = Livro.objects.count()    # Banco conta as linhas
    return render(request, 'contagem.html', {'total': total})
Clique para expandir e ver mais

SQL Gerado:

SQL
SELECT COUNT(*) FROM livro;  # Retorna apenas o número: 1000000
Clique para expandir e ver mais

📊 Comparação de Performance

Registroslen().count()Aceleração
1005ms1ms5x
10.000500ms1ms500x
1.000.00050s2ms25.000x

📝 Quando Usar Cada Um

SituaçãoMétodoPor quê
Precisa SOMENTE da contagem.count()✅ Eficiente
Precisa da contagem E da listalen(queryset) ou .count()Qualquer um funciona
Já tem dados em cachelen(lista_de_objetos)✅ Fine
Verificando se queryset está vazio.exists()✅ Mais eficiente

💡 Dica Pro: O Método .exists()

PYTHON
# ❌ Errado - carrega todos os registros
if len(Livro.objects.filter(ano_publicacao=2024)):
    pass

# ✅ Correto - apenas verifica existência
if Livro.objects.filter(ano_publicacao=2024).exists():
    pass
Clique para expandir e ver mais

Problema 5: Paginação Ausente

🔍 O Que É?

Carregar todos os registros de uma vez sobrecarrega o banco de dados, a memória do servidor e o navegador. Com grandes volumes de dados, isso causa timeouts, crashes e péssima experiência do usuário.

🧠 Por Que Importa?

PYTHON
# Carregando 100.000 livros de uma vez:
livros = Livro.objects.all()  # 100.000 registros

# Uso de memória:
# - Python: ~500MB
# - Banco de dados: 100.000 linhas enviadas
# - Rede: 50MB+ transferidos
# - Navegador: Congela enquanto renderiza 100.000 linhas
Clique para expandir e ver mais

💻 O Código

❌ Versão Lenta:

PYTHON
def paginacao_lenta(request):
    """Carrega TODOS os livros - pode crashar com grandes volumes!"""
    livros = Livro.objects.select_related('autor').all()[:200]
    return render(request, 'livros.html', {'livros': livros})
Clique para expandir e ver mais

✅ Versão Rápida:

PYTHON
from django.core.paginator import Paginator

def paginacao_rapida(request):
    """Carrega apenas 20 livros por vez - eficiente!"""
    qs = Livro.objects.select_related('autor').only(
        'titulo', 'ano_publicacao', 'autor__nome'
    )
    paginator = Paginator(qs, per_page=20)
    page = paginator.get_page(request.GET.get('page', 1))
    return render(request, 'livros.html', {'page': page})
Clique para expandir e ver mais

SQL Gerado (Página 1):

SQL
-- Query 1: Contagem total
SELECT COUNT(*) FROM livro;

-- Query 2: Página atual
SELECT livro.id, livro.titulo, livro.ano_publicacao, autor.nome
FROM livro
INNER JOIN autor ON livro.autor_id = autor.id
LIMIT 20 OFFSET 0;
Clique para expandir e ver mais

📝 Template com Paginação

HTML
<h1>Livros - Página {{ page.number }} de {{ page.paginator.num_pages }}</h1>

<ul>
{% for livro in page.object_list %}
    <li>{{ livro.titulo }} por {{ livro.autor.nome }}</li>
{% endfor %}
</ul>

<nav>
    {% if page.has_previous %}
        <a href="?page={{ page.previous_page_number }}">Anterior</a>
    {% endif %}
    
    <span>Página {{ page.number }}</span>
    
    {% if page.has_next %}
        <a href="?page={{ page.next_page_number }}">Próxima</a>
    {% endif %}
</nav>
Clique para expandir e ver mais

📊 Impacto na Performance

LivrosSem PaginaçãoCom Paginação (20/página)
100100 carregados20 carregados
1.0001.000 carregados20 carregados
100.000💥 Crash20 carregados

🎯 Boas Práticas de Paginação

PYTHON
# Sempre combine com select_related e only
qs = Livro.objects.select_related('autor').only(
    'titulo', 'autor__nome'
)
paginator = Paginator(qs, per_page=20)

# Lide com números de página inválidos
page = paginator.get_page(request.GET.get('page', 1))
# Automaticamente lida com page=0, page=9999, etc.
Clique para expandir e ver mais

Como Identificar Esses Problemas

1. Django Debug Toolbar

Instale o Django Debug Toolbar para ver a contagem de queries por request:

BASH
pip install django-debug-toolbar
Clique para expandir e ver mais

Adicione ao settings.py:

PYTHON
INSTALLED_APPS = [
    'debug_toolbar',
    # ...
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    # ...
]
Clique para expandir e ver mais

Visite /__debug__/ em desenvolvimento para ver as queries.

2. Django Silk

Para profiling em produção:

BASH
pip install django-silk
Clique para expandir e ver mais

Visite /silk/ para ver o profiling de requests.

3. Middleware de Contagem de Queries

Crie um middleware personalizado para contar queries:

PYTHON
# middleware.py
class QueryCountMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        from django.db import connection
        initial = len(connection.queries)
        response = self.get_response(request)
        query_count = len(connection.queries) - initial
        response['X-Query-Count'] = query_count
        return response
Clique para expandir e ver mais

Resumo: Referência Rápida

PYTHON
# ═══════════════════════════════════════════════════════════
# FOLHA DE DICAS DE OTIMIZAÇÃO
# ═══════════════════════════════════════════════════════════

# 1. ForeignKey → select_related (JOIN)
Livro.objects.select_related('autor')

# 2. ManyToMany → prefetch_related (2 queries)
Livro.objects.prefetch_related('tags')

# 3. Carregar apenas campos necessários
Livro.objects.only('titulo', 'preco')

# 4. Contar eficientemente (não len()!)
Livro.objects.count()

# 5. Sempre paginar
from django.core.paginator import Paginator
Paginator(qs, per_page=20)

# ═══════════════════════════════════════════════════════════
# COMBINANDO OTIMIZAÇÕES
# ═══════════════════════════════════════════════════════════

# Melhor: select_related + only + paginate
livros = Livro.objects.select_related('autor').only(
    'titulo', 'ano_publicacao', 'autor__nome'
)
paginator = Paginator(livros, per_page=20)
page = paginator.get_page(request.GET.get('page', 1))
Clique para expandir e ver mais

Experimente Você Mesmo: Aplicação Demo

🚀 Explore a aplicação demo ao vivo e teste todas as otimizações na prática!

A aplicação demo inclui:

ProblemaVersão LentaVersão Rápida
N+1 ForeignKey/books/slow//books/fast/
N+1 ManyToMany/tags/slow//tags/fast/
SELECT */fields/slow//fields/fast/
len() vs count()/count/slow//count/fast/
Paginação/paginate/slow//paginate/fast/

Playground Interativo

Teste todos os problemas interativamente em: /playground/


Conclusão

Esses 5 problemas de performance são os maiores culpados atrás de aplicações Django lentas. A boa notícia? Eles são todos fáceis de resolver uma vez que você sabe o que procurar:

  1. N+1 ForeignKey → Use select_related()
  2. N+1 ManyToMany → Use prefetch_related()
  3. **SELECT *** → Use only() ou defer()
  4. len() → Use .count()
  5. Todos os registros → Use paginação

Comece a aplicar essas otimizações hoje e veja a performance da sua aplicação disparar! 🚀


Referências

Iniciar busca

Digite palavras-chave para buscar

↑↓
ESC
⌘K Atalho