System Design/02 - Componentes/Databases2 min
SQL Tuning
- Que problema isso resolve?
- Quando usar / quando NÃO usar?
- Qual o principal trade-off?
- Como isso falha em produção?
Conceito
Otimizar consultas e schema antes de escalar infraestrutura. É o passo de maior retorno e menor custo — e frequentemente o único necessário.
O primeiro comando
EXPLAIN ANALYZE SELECT ...
O que procurar na saída:
| Sinal | Significa |
|---|---|
| Seq Scan em tabela grande | Falta índice |
| Nested Loop com muitas linhas | Junção ruim; falta índice na chave |
rows estimado ≫ real (ou vice-versa) |
Estatísticas desatualizadas |
| Sort em disco | work_mem insuficiente |
| Filtro aplicado depois do scan | O índice não está sendo usado |
Índices — o que importa
| Conceito | Efeito |
|---|---|
| Índice composto | A ordem das colunas importa: (a, b) serve para a e a+b, não para b sozinho |
| Covering index | Contém todas as colunas da query — não toca a tabela |
| Índice parcial | WHERE ativo = true — menor e mais rápido |
| Seletividade | Índice em coluna com 2 valores distintos raramente ajuda |
| Custo de escrita | Cada índice desacelera INSERT/UPDATE |
O que invalida o índice
Função sobre a coluna (WHERE upper(nome) = ...), LIKE '%x' com curinga à esquerda, tipo incompatível na comparação, e OR entre colunas de índices diferentes.
Trade-offs
Índice é o exemplo mais claro do trade-off leitura/escrita: acelera consulta e desacelera toda escrita, além de ocupar espaço. Tabelas com dezenas de índices "por precaução" têm escrita lenta e índices que ninguém usa — vale auditar quais são efetivamente acionados.
Os problemas mais comuns, em ordem de frequência:
N+1 — uma consulta para a lista e mais uma para cada item. É Chatty IO com roupa de ORM, e a correção é JOIN ou eager loading.
SELECT * — traz colunas que ninguém usa, impede covering index e infla tráfego. Ver Extraneous Fetching.
Paginação por OFFSET — OFFSET 100000 faz o banco descartar 100 mil linhas antes de devolver 20. Paginação por cursor (WHERE id > ?) é constante e estável sob escrita concorrente.
Transação longa — segura locks e, no Postgres, impede a limpeza de versões antigas, inflando a tabela.
Exemplo prático
-- 4,2 s: Seq Scan em 8 milhões de linhas
SELECT * FROM pedidos WHERE cliente_id = 42 ORDER BY criado_em DESC LIMIT 20;
CREATE INDEX idx ON pedidos (cliente_id, criado_em DESC);
-- 3 ms
Três milissegundos contra quatro segundos — mais de mil vezes mais rápido, com uma linha e sem custo de infraestrutura.
A ordem das colunas no índice é o que faz funcionar: cliente_id filtra e criado_em DESC já entrega ordenado, eliminando o passo de Sort. Invertida, o índice serviria muito menos.
É esse tipo de ganho que justifica a regra: meça e ajuste antes de adicionar réplica, cache ou shard.
Relacionado
Parte de Databases · roadmap.sh/system-design