O índice remissivo do livro
Procurar todas as menções a 'idempotência' num livro de 800 páginas folheando página por página levaria a tarde inteira. O índice remissivo no fim do livro — ordenado alfabeticamente — resolve em segundos: você acha a palavra e ela te dá as páginas. Um índice de banco é literalmente isso. E note o custo: cada vez que o livro ganha uma edição nova, o índice remissivo precisa ser refeito. É o que a escrita paga por ter a leitura rápida.
O índice que existe e não é usado
O erro mais comum não é esquecer de criar o índice — é criá-lo e ele não ser usado. O índice é sobre a coluna crua; assim que a consulta envolve essa coluna em algo (LOWER(email), data::text, valor + 10), o banco não reconhece mais o índice e volta ao Seq Scan.
O mesmo vale para LIKE '%texto' (curinga à esquerda impede a busca binária) e para o índice composto usado pela coluna errada. O índice aparece na estrutura da tabela, dá a sensação de que a query está coberta — e só o EXPLAIN revela que ela varreu tudo mesmo assim.
A regra do índice composto derruba muita gente: um índice em (cliente_id, data) serve filtros por cliente_id e por cliente_id AND data, mas não por data sozinha. Pense na lista telefônica ordenada por sobrenome e depois por nome: achar todos os 'Silva' é fácil, achar todos os 'João' de qualquer sobrenome exige varrer tudo. A ordem das colunas no índice é a ordem do catálogo, e ela decide quais consultas ele acelera.
Cardinalidade define se vale a pena. Índice brilha em colunas com muitos valores distintos (e-mail, CPF, id), onde o filtro elimina quase tudo. Numa coluna com três valores possíveis (um status), o índice aponta para um terço da tabela — e pular por ele, salto a salto, sai mais caro que uma varredura sequencial que o disco lê em blocos. Por isso o planejador às vezes ignora um índice de propósito: ele calculou que varrer é mais rápido, e geralmente está certo.
Do outro lado da conta está a escrita. Cada índice numa tabela é uma estrutura a mais para manter: todo INSERT, UPDATE ou DELETE precisa atualizar a tabela e cada índice dela. Cinco índices numa tabela quente significam cinco estruturas reordenadas a cada escrita. Índice não é decoração que se acumula 'por garantia' — é uma aposta de que a leitura daquela coluna vale o imposto cobrado em toda escrita.
Com índice (Index Scan)
- O banco desce a B-tree ordenada por busca binária
- Custo cresce com o logaritmo do número de linhas
- 10 milhões de linhas = ~23 passos
- Cada escrita agora mantém a tabela e o índice
Leitura rápida em qualquer tamanho — paga com espaço e com escrita um pouco mais lenta.
Como isso aparece na prática
O login que ficava lento à noite
Cenário
A tela de login buscava o usuário por e-mail. Com a base pequena, era instantâneo; ao passar de alguns milhões de usuários, o login começou a demorar segundos nos horários de pico.
Como o padrão entra
Um índice em email transformou o Seq Scan da tabela inteira em um Index Scan de poucos passos, e o e-mail passou a ser guardado já em minúsculas para a busca casar com o índice sem função.
Trade-off: Cada cadastro e atualização de e-mail passou a manter o índice também. Numa tabela de leitura muito mais intensa que a de escrita, foi um troco irrisório pelo login instantâneo.
A tabela com dez índices que engasgava na escrita
Cenário
Uma tabela de eventos, escrita a milhares de linhas por segundo, tinha acumulado dez índices ao longo do tempo — cada um criado para acelerar 'aquela consulta específica'. As inserções começaram a formar fila.
Como o padrão entra
O EXPLAIN e as estatísticas de uso mostraram que metade dos índices quase nunca era usada. Eles foram removidos, e a latência de escrita caiu pela metade sem impacto perceptível nas leituras.
Trade-off: Duas consultas raras voltaram a ser mais lentas. Aceitável: elas rodavam uma vez por dia, enquanto a escrita acontecia o tempo todo e era o gargalo real.
A função que anula o índice
Compila, roda, passa no code review. E quebra depois.
O índice foi criado, aparece na estrutura da tabela e todo mundo assume que a consulta está coberta. Só que a query envolve a coluna numa função — LOWER, um cast, uma aritmética — e o banco, que só reconhece o índice sobre a coluna crua, silenciosamente volta a varrer a tabela inteira. Rápido em desenvolvimento com poucos dados, lento em produção com milhões.
// Existe indice em "email" — a busca deveria ser instantanea:
// CREATE INDEX idx_usuarios_email ON usuarios (email);
// Mas a consulta envolve a coluna numa funcao:
const sql = "SELECT * FROM usuarios WHERE LOWER(email) = $1";
// ^^^^^^^^^^^^
// O indice e sobre "email", nao sobre "LOWER(email)".
// O banco nao consegue usa-lo: cai em Seq Scan e varre a tabela.
//
// O mesmo vale para outras transformacoes na coluna:
// WHERE email LIKE '%@x.com' (curinga a esquerda)
// WHERE data::text = '2026-08-11' (cast na coluna)
// WHERE valor + 10 > 100 (aritmetica na coluna)
//
// O indice existe, aparece no \d da tabela e da a sensacao de
// que a query esta coberta — e nao esta. So o EXPLAIN revela.O que quebra
- Em produçãoA consulta que parecia indexada vira Seq Scan sobre milhões de linhas, e a latência dispara sem nenhuma mudança de código aparente.
- Com base pequenaSome: com poucos dados o Seq Scan é rápido, então o problema não aparece em desenvolvimento nem nos testes.
- Ao olhar só a tabelaO índice está lá no
\dda tabela, o que engana o diagnóstico — só o EXPLAIN mostra que ele não foi usado.
A saída: Filtre pela coluna crua: guarde o dado já normalizado (o e-mail em minúsculas) e escreva WHERE email = $1. Quando a transformação for inevitável, crie um índice de expressão sobre exatamente ela (CREATE INDEX ... ON usuarios (LOWER(email))). E confirme no EXPLAIN que o plano virou Index Scan.
Onde isto dá errado
Envolver a coluna indexada numa função
WHERE LOWER(email) = ..., um cast ouLIKE '%x'fazem o banco ignorar o índice da coluna crua e cair em Seq Scan. O índice existe, mas não para essa consulta — só o EXPLAIN denuncia.Indexar coluna de baixa cardinalidade
Um índice sobre um booleano ou um status de três valores aponta para uma fração enorme da tabela. Pular por ele sai mais caro que varrer em blocos, e o planejador acaba ignorando o índice que você pagou para manter.
Acumular índices 'por garantia'
Cada índice é pago em toda escrita. Numa tabela quente, uma dúzia de índices — metade nunca usada — transforma inserção em gargalo. Índice se cria pela consulta real e se remove quando o EXPLAIN mostra que ninguém o usa.
Errar a ordem do índice composto
Um índice
(a, b)serve filtros porae pora AND b, mas não porbsozinho. Criar o composto na ordem errada é criar um índice que a consulta que você queria acelerar não consegue usar.
A B-tree, o EXPLAIN e a regra da coluna
// Um indice e uma copia ORDENADA de uma coluna (uma B-tree),
// mantida em paralelo a tabela. Ordenada = da para achar por
// busca binaria em vez de varrer linha a linha.
// SEM indice: achar 1 email em 10 milhoes = ler 10 milhoes (Seq Scan).
// COM indice: achar 1 email em 10 milhoes = ~23 passos (log2 de 10M).
// Diagnostico: o EXPLAIN diz se a query USOU o indice.
// Index Scan using idx_usuarios_email -> usou (bom)
// Seq Scan on usuarios -> nao usou (varreu tudo)
// A regra da coluna: o indice serve o filtro so se a coluna
// aparece "crua" na condicao — sem funcao, sem cast, sem curinga
// a esquerda. Por isso guarda-se o dado ja normalizado:
await db.insert("usuarios", { email: email.toLowerCase() });
// e a busca fica: WHERE email = $1 -> usa o indice, direto.
// Indice composto (a, b) serve filtros por "a" e por "a AND b",
// mas NAO por "b" sozinho: a ordem das colunas e a ordem do catalogo.Quando usar × quando evitar
Use quando
3- Colunas frequentes em WHERE, JOIN e ORDER BY.
- Tabelas grandes onde Seq Scan vira segundos.
- Para impor unicidade com um índice único.
Evite quando
2- Colunas de baixa cardinalidade.
- Tabelas pequenas ou de escrita muito intensa.
Testar o que ficou
4 armadilhas de Índice de banco viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
CREATE INDEXO comando que constrói a estrutura ordenada da coluna para a busca deixar de varrer a tabela inteira.Index Scan × Seq Scan no EXPLAINO plano de execução mostra se a query usou o índice ou desistiu dele e varreu tudo — o diagnóstico do desempenho.a PRIMARY KEYToda chave primária cria um índice único automaticamente; é o índice que quase todo mundo usa sem perceber que criou.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Cópia ordenada de uma coluna: acha por busca binária.
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);O que ele cobra
Nenhum salto a mais entre a chamada e o efeito.
- Cada INSERT, UPDATE e DELETE passa a manter também o índice — escrita fica mais lenta a cada índice na tabela
- Ocupa espaço em disco proporcional aos dados indexados, e um índice grande pode não caber na memória
- Dá falsa sensação de cobertura: o índice existe, mas a query com função ou curinga à esquerda não o usa
Não vale se: a coluna tem pouquíssimos valores distintos ou a tabela é pequena. Varrer mil linhas é mais rápido que pular por um índice que quase não filtra.
Onde isto aparece
Continue por aqui
- ShardingQuando os dados não cabem mais numa máquina — nem em disco, nem em escrita —, a saída é reparti-los: cada shard guarda um pedaço, escolhido por uma chave. Escala escrita e volume de forma horizontal, ao preço alto de perder as junções fáceis, as transações entre shards e a chance de errar a chave e nunca mais conseguir corrigir barato.
- Níveis de isolamentoTransações concorrentes se atrapalham de formas específicas e nomeadas. O nível de isolamento é a escolha de quais dessas anomalias o banco vai impedir — e cada degrau a mais custa concorrência, ou custa transações abortadas.
- Lock otimista × pessimistaDuas formas de impedir que duas escritas concorrentes se apaguem. O pessimista trava antes e faz todo mundo esperar; o otimista não trava nada e descobre o conflito na hora de gravar. A escolha é sobre a probabilidade de conflito, não sobre preferência.