# Django e PostgreSQL em 2026: índices, busca full-text e questões de entrevista > Guia prático de otimização de Django com PostgreSQL: índices B-tree, parciais e de cobertura, busca full-text com SearchVector e GIN, além de questões de entrevista de 2026. - Published: 2026-06-27 - Updated: 2026-07-06 - Author: SharpSkill - Tags: django, postgresql, database optimization, full text search, python - Reading time: 9 min --- A otimização de Django com PostgreSQL é o que separa uma aplicação que escala além dos primeiros mil usuários de outra que trava a cada listagem. O PostgreSQL vem com um mecanismo de índices e um subsistema de busca full-text que a maioria dos projetos Django nunca toca, desperdiçando um ganho de desempenho que não custa nada para recuperar. Este guia aborda os padrões de indexação, as ferramentas de busca de `django.contrib.postgres` e as questões de entrevista sobre banco de dados que separam pessoas desenvolvedoras Django plenas das sêniores em 2026. > **Meça antes de indexar** > > Todo índice acelera as leituras, mas torna as escritas mais lentas e consome disco. Rode `EXPLAIN (ANALYZE, BUFFERS)` na consulta real, adicione o índice e compare o plano. Um índice que o planejador de consultas nunca escolhe é puro custo extra em cada INSERT e UPDATE. ## Fundamentos de indexação no PostgreSQL com Django Por padrão, o PostgreSQL cria um índice B-tree apenas nas chaves primárias e nas colunas com `unique=True`. Toda outra coluna usada em filtro ou ordenação executa uma varredura sequencial até que exista um índice. O Django expõe os índices por meio de `Meta.indexes`, opção preferível ao antigo `db_index=True` porque concentra em um só lugar os índices compostos, parciais e de cobertura. A ordem das colunas dentro de um índice composto não é detalhe estético. Uma B-tree só consegue usar o índice em uma consulta quando o filtro cobre um prefixo inicial das colunas indexadas, regra detalhada em [Use The Index, Luke](https://use-the-index-luke.com/sql/where-clause/the-equals-operator/concatenated-keys). Um índice em `(status, created_at)` atende `WHERE status = ? ORDER BY created_at`, mas o mesmo índice não acelera uma consulta que filtra apenas por `created_at`. ```python # models.py from django.db import models class Order(models.Model): customer_email = models.EmailField() status = models.CharField(max_length=20) total = models.DecimalField(max_digits=10, decimal_places=2) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [ # B-tree de coluna única para buscas exatas e por faixa em status models.Index(fields=["status"]), # Índice composto: atende WHERE status = ? ORDER BY created_at DESC # A coluna inicial (status) precisa aparecer no filtro para ser usada models.Index(fields=["status", "-created_at"]), ] ``` O índice composto transforma uma consulta de dashboard como "últimos pedidos pendentes" em uma única varredura de índice, em vez de filtrar e ordenar a tabela inteira. ## Índices parciais e de cobertura para consultas específicas Um índice parcial cobre apenas as linhas que satisfazem uma condição, então permanece pequeno e cabe na memória. Quando a maioria das linhas compartilha um mesmo valor e as consultas só tocam a minoria, um índice parcial fica muito mais barato do que um completo. Um índice de cobertura vai além: ao acrescentar colunas fora da chave com `include`, o PostgreSQL responde à consulta direto do índice sem tocar o heap da tabela, padrão de acesso chamado de index-only scan. ```python # models.py from django.db import models from django.db.models import Q class Order(models.Model): # campos definidos acima class Meta: indexes = [ # Índice parcial: indexa apenas linhas pendentes, ignorando o histórico concluído models.Index( fields=["created_at"], name="pending_orders_idx", condition=Q(status="pending"), ), # Índice de cobertura: total é lido direto do índice (index-only scan) models.Index( fields=["status"], include=["total"], name="status_total_covering_idx", ), ] ``` Em uma tabela de pedidos onde 2% das linhas estão pendentes, o índice parcial pode ser cinquenta vezes menor do que um índice completo na mesma coluna, e o planejador o mantém aquecido no buffer cache. A cláusula `include` está documentada em [index-only scans do PostgreSQL](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) e é o ganho mais ignorado em aplicações Django que agregam uma coluna numérica atrás de um filtro por status. ## Busca full-text no Django com PostgreSQL Recorrer a `title__icontains=query` parece prático, mas força uma varredura sequencial e não consegue ordenar os resultados por relevância. A busca full-text do Django encapsula os tipos nativos `tsvector` e `tsquery` do PostgreSQL por meio de `SearchVector`, `SearchQuery` e `SearchRank`. Um vetor de busca normaliza o texto em lexemas, remove as stop words e reduz as palavras aos seus radicais, de modo que uma busca por "running" encontra "run" e "ran". ```python # views.py from django.contrib.postgres.search import ( SearchVector, SearchQuery, SearchRank, ) from .models import Article def search_articles(query_text): query = SearchQuery(query_text, config="english") # O peso A classifica correspondências no título acima das do corpo (peso B) vector = ( SearchVector("title", weight="A") + SearchVector("body", weight="B") ) return ( Article.objects.annotate(rank=SearchRank(vector, query)) .filter(rank__gte=0.1) .order_by("-rank") ) ``` O parâmetro `weight` distribui faixas de prioridade de A (mais alta) até D. Uma correspondência no título fica acima de uma no corpo mesmo quando ambos contêm o termo buscado, o que coincide com o comportamento que os usuários esperam de uma busca. A API completa e o modelo de pesos em quatro níveis estão descritos na [referência de busca full-text do Django](https://docs.djangoproject.com/en/5.2/ref/contrib/postgres/search/). ## Índices GIN e SearchVectorField para buscas rápidas A consulta acima recalcula o vetor de busca para cada linha a cada requisição, o que funciona bem para alguns milhares de linhas e é inaceitável para um milhão. O padrão de produção armazena o vetor em um `SearchVectorField` e o indexa com um índice GIN, o tipo de índice que o PostgreSQL usa para valores compostos como documentos full-text e JSONB. ```python # models.py from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex from django.db import models class Article(models.Model): title = models.CharField(max_length=255) body = models.TextField() # Documento de busca armazenado e pré-computado search_vector = SearchVectorField(null=True) class Meta: indexes = [ # O GIN transforma buscas full-text em varreduras de índice logarítmicas GinIndex(fields=["search_vector"]), ] ``` O campo armazenado precisa se manter sincronizado com `title` e `body`. Um sinal `post_save` que grava o vetor com `.update()` evita disparar o sinal de forma recursiva, já que `QuerySet.update()` não emite sinais de save. ```python # signals.py from django.contrib.postgres.search import SearchVector from django.db.models.signals import post_save from django.dispatch import receiver from .models import Article @receiver(post_save, sender=Article) def sync_search_vector(sender, instance, **kwargs): Article.objects.filter(pk=instance.pk).update( search_vector=SearchVector("title", weight="A") + SearchVector("body", weight="B"), ) ``` Para times em Django 5.0 ou mais recente, um `GeneratedField` calculado no banco ou um trigger do PostgreSQL elimina por completo o UPDATE extra ao manter a coluna dentro do próprio banco. Seja qual for o mecanismo que a mantém preenchida, a consulta passa a filtrar o campo indexado diretamente, e o índice GIN transforma o que era uma varredura completa de tabela em uma busca logarítmica. > **Índices GIN são caros para escrever** > > Índices GIN aceleram as leituras a um custo real de escrita, pois cada inserção mexe em muitas entradas do índice. Em tabelas com muitas escritas, defina o parâmetro de armazenamento `fastupdate` e monitore o tamanho da lista pendente, ou as escritas full-text vão ficar travadas atrás da manutenção do índice. ## Escolhendo o tipo de índice certo no PostgreSQL A B-tree é o padrão correto, mas o PostgreSQL oferece vários tipos de índice por meio de `django.contrib.postgres.indexes`, e escolher o errado desperdiça disco sem melhorar uma única consulta. A escolha segue o formato dos dados e o operador que a consulta usa, conforme catalogado na [documentação de tipos de índice do PostgreSQL](https://www.postgresql.org/docs/current/indexes-types.html). | Tipo de índice | Melhor para | Classe do Django | |------------|----------|--------------| | B-tree | Igualdade e faixa em colunas escalares | `models.Index` | | GIN | Busca full-text, JSONB, contenção em arrays | `GinIndex` | | BRIN | Tabelas enormes append-only ordenadas por uma coluna | `BrinIndex` | | Hash | Buscas apenas por igualdade em colunas grandes | `HashIndex` | O BRIN merece atenção especial em dados de série temporal. Em uma tabela com dezenas de milhões de linhas inseridas em ordem de timestamp, um `BrinIndex` em `created_at` ocupa alguns kilobytes onde uma B-tree precisaria de centenas de megabytes, porque o BRIN guarda apenas os valores mínimo e máximo por faixa de blocos. O custo é que o BRIN só ajuda quando a ordem física das linhas acompanha a coluna indexada, o que ocorre naturalmente em logs append-only e tabelas de eventos. ```python # models.py from django.contrib.postgres.indexes import BrinIndex from django.db import models class Event(models.Model): payload = models.JSONField() created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [ # Índice minúsculo para varreduras por faixa em uma tabela append-only ordenada por tempo BrinIndex(fields=["created_at"]), ] ``` Alinhar o tipo de índice ao padrão de acesso é um tema recorrente em entrevistas sêniores, porque demonstra que a pessoa candidata raciocina sobre a organização do armazenamento em vez de sair adicionando B-trees por reflexo. ## Questões de entrevista sobre banco de dados no Django em 2026 As questões sobre banco de dados dominam as entrevistas Django sêniores porque fluência no ORM não implica entender o que o ORM gera. As perguntas a seguir refletem o que as bancas de contratação realmente investigam em 2026, e combinam naturalmente com o ajuste no nível de consulta abordado no [guia de otimização de consultas do Django ORM](/blog/django/django-orm-optimizing-queries). **Quando um índice composto em `(a, b)` deixa de ajudar uma consulta?** Quando a consulta não filtra por `a`. Uma B-tree é ordenada primeiro pela coluna inicial, então um filtro somente por `b` não consegue usar o índice. Essa é a regra do prefixo mais à esquerda, e quem a explica demonstra entender a estrutura do índice em vez de decorar receitas. **Qual é a diferença entre `select_related` e `prefetch_related`?** `select_related` executa um JOIN em SQL e funciona para relações de chave estrangeira e um-para-um em uma única consulta. `prefetch_related` dispara uma segunda consulta e junta os resultados em Python, o que é necessário para relações muitos-para-muitos e chaves estrangeiras reversas. Usar o errado ou perde a otimização ou dispara uma segunda consulta desnecessária. **Como o `icontains` difere da busca full-text no nível do banco?** `icontains` compila para `ILIKE '%term%'`, que não consegue usar um índice B-tree padrão e varre todas as linhas. A busca full-text compara com um `tsvector` pré-computado apoiado em um índice GIN e ordena os resultados por relevância. Essa distinção é um sinal confiável de que a pessoa candidata já passou dos conjuntos de dados de brinquedo. **Por que `count()` pode ser lento em uma tabela grande e quais são as alternativas?** O PostgreSQL não mantém uma contagem de linhas em cache para a tabela, então `COUNT(*)` varre todas as linhas visíveis sob o MVCC. Para contagens aproximadas, consultar `pg_class.reltuples` devolve uma estimativa na hora e, para paginação, a paginação por keyset em uma coluna indexada dispensa a contagem por completo. Prática estruturada sobre esses padrões está disponível no [módulo de entrevista sobre cache no Django](/technologies/django/interview-questions/django-caching), onde contadores apoiados em cache são um tema recorrente, e em toda a [trilha de aprendizado de Django](/technologies/django). **O que `EXPLAIN ANALYZE` revela que `EXPLAIN` sozinho não mostra?** `EXPLAIN` imprime o custo estimado pelo planejador; `EXPLAIN ANALYZE` de fato executa a consulta e informa tempos e contagens de linhas reais. Uma grande diferença entre linhas estimadas e reais aponta para estatísticas de tabela desatualizadas, corrigíveis com `ANALYZE`, e é uma causa raiz comum de o planejador ignorar um índice que obviamente deveria se aplicar. ## Conclusão - Defina os índices em `Meta.indexes` em vez de `db_index=True` para que índices compostos, parciais e de cobertura fiquem em um único lugar auditável. - Ordene as colunas de um índice composto pela regra do prefixo mais à esquerda: a coluna inicial precisa aparecer no filtro da consulta para o índice ser usado. - Use índices parciais quando as consultas só tocam uma minoria das linhas, como pedidos pendentes em uma tabela dominada por histórico concluído. - Acrescente índices de cobertura com `include` para habilitar index-only scans e evitar o acesso ao heap em consultas de agregação com muitas leituras. - Substitua a busca com `icontains` por `SearchVector`, `SearchQuery` e `SearchRank` para ganhar ordenação por relevância e stemming de graça. - Armazene o documento de busca em um `SearchVectorField` apoiado por um `GinIndex`, mantido sincronizado por um sinal, `GeneratedField` ou trigger de banco, antes de a busca full-text chegar à escala de produção. - Sempre valide as decisões de índice com `EXPLAIN (ANALYZE, BUFFERS)`; um índice que o planejador nunca seleciona é um imposto no momento da escrita sem nenhum benefício na leitura. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/django/django-postgresql-indexing-full-text-search-2026