Índices SQL e Árvores B: Como Otimizar Queries com EXPLAIN ANALYZE

Índices SQL e Árvores B: Como Otimizar Queries com EXPLAIN ANALYZE

O Abismo do Milhão de Linhas: Porque as Queries Sem Índice Falham em Produção

Quando desenvolves uma aplicação na tua máquina local, o desempenho pode ser ilusório. Criaste quinhentos utilizadores falsos para testar, escreveste uma consulta como SELECT * FROM encomendas WHERE cliente_id = 45 AND estado = 'pendente' e viste o resultado surgir em 1 milissegundo.

Seis meses depois de a plataforma ser lançada a clientes reais, o servidor começa a arrastar-se. A CPU da base de dados cola nos 100%. Pedidos de API que demoravam 30 milissegundos passam a demorar 4 segundos e os clientes começam a queixar-se de lentidão extrema.

O que mudou? A tua tabela cresceu de 500 linhas para 1,5 milhões de registos.

Como a tabela encomendas não tinha um índice nas colunas (cliente_id, estado), a base de dados foi forçada a fazer um Sequential Scan (Seq Scan): ler absolutamente todos os blocos do disco físico para a memória RAM, comparar um milhão e meio de linhas uma a uma e deitar fora 99.9% delas para devolver apenas quatro registos.

  • SEQUENTIAL SCAN (O(N)) vs B-TREE INDEX SCAN (O(log N))
  • SEQUENTIAL SCAN (Sem Índice): Lê Página 1 → Lê Página 2 → Lê Página 3 … → Lê Página 45.000! (Analisa 1.500.000 linhas em sequência: 4.200ms! Esgota o disco e a CPU)
  • B-TREE INDEX SCAN (Com Índice): [Nó Raiz]
  • Ramo 1: [Ramo 2]
  • Folha A: [Folha B] → Aponta diretamente para o Bloco #314 do Disco! (Consulta 3 nós na memória RAM: 0.6ms! Dezenas ou centenas de vezes mais rápido)

No desenvolvimento de web apps e websites de alta performance, dominar a indexação e saber ler planos de execução com o EXPLAIN ANALYZE é obrigatório.

A Estrutura de Uma Árvore B: Como a Base de Dados Encontra Dados em Milissegundos

O índice padrão e mais versátil no PostgreSQL e MySQL é a Árvore B (B-Tree ou Balanced Tree).

A Árvore B organiza as chaves de pesquisa numa estrutura equilibrada desenhada propositadamente para minimizar o número de acessos ao disco:

  1. O Nó Raiz (Root Node): A página inicial de entrada mantida permanentemente em memória cache.
  2. Os Nós Intermédios (Branch Nodes): Páginas de encaminhamento que contêm intervalos de valores e ponteiros para as páginas filhas.
  3. Os Nós Folha (Leaf Nodes): O nível inferior da árvore que guarda as chaves ordenadas e os respetivos Tuple IDs (TIDs), que indicam exatamente o ficheiro, bloco e posição física da linha no disco.

NAVEGAÇÃO INTERNA NUMA ÁRVORE B

  • Consulta: WHERE id_utilizador = 68
  • NÓ RAIZ (Página 0)
  • Chaves: [20] | [70]
  • (68 está entre 20 e 70)
  • NÓ DE RAMO (Pág 3): Chaves: [35] | [55]
  • NÓ DE RAMO (Pág 4): Chaves: [80] | [90]
  • (68 > 55: Segue pelo ponteiro da direita)
  • NÓ FOLHA (Página 11): Chaves e Ponteiros
  • Chave 66 → TID: (Bloco 102, Posição 2)
  • Chave 68 → TID: (Bloco 314, Posição 7) → [Lê a Linha Real]
  • Chave 69 → TID: (Bloco 450, Posição 1)

Como cada página da Árvore B guarda centenas de ponteiros, uma tabela com dezenas de milhões de linhas precisa apenas de 3 ou 4 saltos de memória para encontrar o registo pretendido. Em vez de ler gigabytes de ficheiros, a base de dados lê alguns kilobytes.

Como Interpretar o EXPLAIN (ANALYZE, BUFFERS)

Muitos programadores cometem o erro de correr apenas o comando EXPLAIN. O EXPLAIN simples mostra apenas a suposição teórica calculada pelo motor de estatísticas.

Para veres o que realmente aconteceu durante a execução, deves utilizar sempre EXPLAIN (ANALYZE, BUFFERS):

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, valor_total 
FROM encomendas 
WHERE cliente_id = 104 AND estado = 'pendente';

Compara o comportamento antes e depois de criares o índice:

Antes da Otimização (O Problema)

Seq Scan on encomendas  (cost=0.00..38540.00 rows=120 width=16) (actual time=35.120..382.400 rows=10 loops=1)
  Filter: ((cliente_id = 104) AND ((estado)::text = 'pendente'::text))
  Rows Removed by Filter: 1499990
  Buffers: shared hit=3100 read=11200
Planning Time: 0.160 ms
Execution Time: 382.550 ms
  • Rows Removed by Filter: 1.499.990: O motor leu um milhão e meio de linhas para aproveitar apenas dez!
  • Buffers: read=11200: O servidor teve de ir ao disco físico ler 11.200 blocos.
  • Execution Time: 382.550 ms: Uma resposta inaceitável para uma API moderna.

Criar o Índice Adequado

CREATE INDEX CONCURRENTLY idx_encomendas_cliente_estado 
ON encomendas (cliente_id, estado);

Depois da Otimização (A Solução)

Index Scan using idx_encomendas_cliente_estado on encomendas  (cost=0.43..11.20 rows=120 width=16) (actual time=0.035..0.065 rows=10 loops=1)
  Index Cond: ((cliente_id = 104) AND ((estado)::text = 'pendente'::text))
  Buffers: shared hit=4
Planning Time: 0.190 ms
Execution Time: 0.080 ms
  • Execution Time: 0.080 ms: O teu pedido ficou quase 4.800 vezes mais rápido!
  • Buffers: shared hit=4: Tocou apenas em 4 páginas na memória RAM, sem qualquer leitura física de disco.

Testar estas queries em contentores com Docker permite afinar o desempenho antes de lançares código em produção.

Índices Avançados: Parciais, Compostos e Covering Indexes

Nem todos os índices precisam de ser iguais. Podes tirar partido de estratégias especializadas:

1. Índices Parciais (Filtrados)

Se 95% das tuas encomendas já estiverem marcadas como estado = 'concluida' e as tuas pesquisas procuram apenas encomendas ativas, não deves indexar as linhas antigas:

CREATE INDEX idx_encomendas_ativas ON encomendas (criado_em) 
WHERE estado IN ('pendente', 'em_processamento');

Este índice fica 95% mais pequeno, cabe todo na memória RAM e não abranda a inserção de encomendas concluídas.

2. Covering Indexes com a Cláusula INCLUDE

Quando um índice encontra a linha, precisa normalmente de ir à tabela principal ler as outras colunas. Ao incluires as colunas adicionais nas folhas da árvore, a base de dados faz um Index Only Scan sem tocar na tabela:

CREATE INDEX idx_consulta_rapida ON encomendas (cliente_id, estado) 
INCLUDE (valor_total, email_contacto);

Podes encontrar exemplos complementares destas técnicas no ecossistema do GitHub.

Boas Práticas Operacionais: Criação sem Bloqueios e Manutenção

Para manteres a consistência e a velocidade das tuas bases de dados segundo os padrões da WebXtek, aplica estas três recomendações:

  1. Usa Sempre CREATE INDEX CONCURRENTLY: No PostgreSQL de produção, um CREATE INDEX normal bloqueia todas as escritas na tabela até o índice estar concluído. O parâmetro CONCURRENTLY cria o índice em segundo plano sem impedir os teus clientes de comprarem ou registarem dados.
  2. A Ordem das Colunas Importa: Numa chave composta (cliente_id, estado), o índice serve para pesquisas apenas por cliente_id, mas não funciona para pesquisas que filtrem apenas por estado. Coloca sempre a coluna com maior poder de distinção à esquerda.
  3. Limpa Índices Bloated Regularmente: Com o passar do tempo e muitas atualizações de dados, as folhas da Árvore B acumulam espaços vazios (bloat). Executa reindexações periódicas para reorganizar as páginas:
    REINDEX INDEX CONCURRENTLY idx_encomendas_cliente_estado;

Se a tua empresa precisa de acelerar aplicações complexas ou auditar o desempenho de bases de dados relacionais sob carga pesada, a nossa equipa de consultoria técnica apoia o desenho e otimização das tuas consultas SQL.

Na prática, aplicas exatamente estes padrões operacionais em setores exigentes como saas empresarial b2b e especialistas em devops e arquitetura cloud. Quando alinhas a tua estrutura através de web apps e websites de alta performance, transformas boas práticas técnicas numa vantagem competitiva real e mensurável para o teu negócio.

[+] [ INDEX_DIRECTORY ] 18 tópicos ▼
[ SYSTEM.FAQ ]

Perguntas Frequentes

Qual é a diferença entre um Sequential Scan (Seq Scan) e um Index Scan?

No Sequential Scan a base de dados lê todos os blocos do disco do início ao fim (complexidade O(N)). No Index Scan o motor percorre uma estrutura equilibrada de Árvore B em O(log N) operações, recolhendo apenas os ponteiros exatos das linhas que cumprem os critérios de pesquisa.

Como é que uma Árvore B (B-Tree) encontra os dados no PostgreSQL?

A Árvore B é composta pelo Nó Raiz, Nós Intermédios e Nós Folha. O motor compara o valor pesquisado com as fronteiras de cada página, descendo pelos ponteiros até encontrar a página folha com o Tuple ID (TID) que aponta para o bloco físico exato no disco.

Porque deves usar EXPLAIN (ANALYZE, BUFFERS) em vez de apenas EXPLAIN?

O comando EXPLAIN simples mostra apenas as estimativas teóricas do otimizador. O EXPLAIN (ANALYZE, BUFFERS) corre a query de verdade, medindo o tempo real de execução em milissegundos e revelando quantas páginas foram lidas da memória RAM face ao disco físico.

Quando é que criar índices a mais prejudica a base de dados?

Cada índice adicionado a uma tabela tem de ser atualizado obrigatoriamente a cada INSERT, UPDATE e DELETE. Criar índices desnecessários torna as escritas lentas, consome espaço em disco e enche a memória cache da base de dados.

> INICIAR_PROJETO

Precisa de um website que transmita confiança, apareça na pesquisa e dê mais força à sua presença digital? Comece a conversa aqui.