kb.erickguedes.com
PostgreSQL: Banco Relacional Avançado

Índices e Performance

Aula 3 de 7

Tipos de Índice

B-tree (Padrão)

-- B-tree: ideal para igualdade e range (=, <, >, BETWEEN, LIKE 'prefixo%')
CREATE INDEX idx_clientes_nome ON clientes(nome);
CREATE INDEX idx_pedidos_data ON pedidos(criado_em);
CREATE INDEX idx_clientes_email ON clientes(email);

-- Índice multicoluna (ordem importa!)
CREATE INDEX idx_clientes_nome_email ON clientes(nome, email);
-- Usa: WHERE nome = 'João'
-- Usa: WHERE nome = 'João' AND email LIKE '%@'
-- NÃO usa: WHERE email = '[email protected]' (precisa de nome primeiro)

Hash

-- Hash: apenas igualdade (=)
CREATE INDEX idx_pedidos_hash_id ON pedidos USING HASH (id);
-- Útil quando a coluna tem muitos valores únicos e só consultas de igualdade

GiST

-- GiST: dados geométricos, full-text, range types
CREATE EXTENSION IF NOT EXISTS btree_gist;  -- range types

-- Geometric queries
CREATE INDEX idx_locais_coords ON locais USING GIST (coordenadas);
SELECT * FROM locais
WHERE coordenadas <@ BOX '(0,0),(100,100)';  -- dentro do box

-- Range types (evitar overlap)
CREATE TABLE reservas (
    sala_id INTEGER,
    periodo TSTZRANGE,
    EXCLUDE USING GIST (sala_id WITH =, periodo WITH &&)
);

GIN

-- GIN: arrays, JSONB, full-text
CREATE INDEX idx_produtos_tags ON produtos USING GIN (tags);
SELECT * FROM produtos WHERE tags @> ARRAY['promocao'];

CREATE INDEX idx_produtos_metadata ON produtos USING GIN (metadata);
SELECT * FROM produtos WHERE metadata @> '{"cor": "azul"}';

-- Full-text search
CREATE INDEX idx_artigos_texto ON artigos USING GIN (to_tsvector('portuguese', texto));
SELECT * FROM artigos
WHERE to_tsvector('portuguese', texto) @@ to_tsquery('portuguese', 'banco & dados');

BRIN

-- BRIN: dados naturalmente ordenados (logs, séries temporais)
-- Muito menor que B-tree, ideal para grandes tabelas
CREATE INDEX idx_logs_brin ON logs USING BRIN (criado_em)
WITH (pages_per_range = 32);

-- Consulta por range de data
SELECT * FROM logs WHERE criado_em BETWEEN '2024-01-01' AND '2024-01-07';
-- BRIN escaneia blocos relevantes ao invés de índice completo

Covering Indexes

-- INCLUDE: adiciona colunas sem ocupar espaço na árvore de busca
CREATE INDEX idx_pedidos_cliente_data ON pedidos(cliente_id, criado_em)
INCLUDE (total, status);

-- Query coberta pelo índice (index-only scan)
SELECT total, status FROM pedidos
WHERE cliente_id = 123 AND criado_em > '2024-01-01';

Partial Indexes

-- Índice apenas em dados relevantes
CREATE INDEX idx_pedidos_ativos ON pedidos(criado_em)
WHERE status NOT IN ('cancelado', 'entregue');

CREATE INDEX idx_usuarios_ativos ON usuarios(email)
WHERE ativo = true;

-- Menor que índice completo
-- Útil para consultas frequentes com filtro específico

Expression Indexes

-- Índice em expressão/função
CREATE INDEX idx_clientes_nome_lower ON clientes(LOWER(nome));
SELECT * FROM clientes WHERE LOWER(nome) = 'joão';

CREATE INDEX idx_pedidos_mes ON pedidos(EXTRACT(YEAR FROM criado_em), EXTRACT(MONTH FROM criado_em));
SELECT * FROM pedidos
WHERE EXTRACT(YEAR FROM criado_em) = 2024 AND EXTRACT(MONTH FROM criado_em) = 1;

EXPLAIN

-- Análise de plano de execução
EXPLAIN SELECT * FROM clientes WHERE id = 123;

-- Com estatísticas reais (EXECUTA a query!)
EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 456;

-- Com buffers e formato JSON
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT p.id, c.nome
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.status = 'pendente';

Node Types

-- Seq Scan: varredura sequencial (tabela inteira)
EXPLAIN SELECT * FROM pedidos WHERE status = 'pendente';
-- Otimizar com índice parcial

-- Index Scan: varredura no índice + busca na tabela
EXPLAIN SELECT * FROM pedidos WHERE id = 123;

-- Index Only Scan: índice cobre a consulta (sem acessar tabela)
EXPLAIN SELECT cliente_id, criado_em FROM pedidos
WHERE cliente_id = 456;

-- Bitmap Scan: combina múltiplos índices
EXPLAIN SELECT * FROM pedidos
WHERE cliente_id = 123 AND criado_em > '2024-06-01';

-- Nested Loop: para cada linha externa, busca na interna
EXPLAIN ANALYZE SELECT * FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE c.id = 123;

-- Hash Join: constrói hash de uma tabela, varre a outra
EXPLAIN ANALYZE SELECT * FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id;

-- Merge Join: ordena ambas, merge sequential
EXPLAIN ANALYZE SELECT * FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
ORDER BY c.id;

Query Planner — Configuração

-- Parâmetros que afetam o planejador

-- Custo de leitura sequencial vs random
SHOW random_page_cost;         -- default: 4.0 (HDD), 1.1 (SSD)
-- Reduzir para SSD melhora escolha por Index Scan

-- Tamanho efetivo do cache do sistema
SHOW effective_cache_size;     -- default: 4GB
-- Aumentar se servidor dedicado PostgreSQL

-- Memória para sorting e hash
SHOW work_mem;                 -- default: 4MB (por operação!)
-- Aumentar com cuidado (cada query pode usar várias operações)

SHOW maintenance_work_mem;     -- default: 64MB
-- Usado em VACUUM, CREATE INDEX, ADD FOREIGN KEY

VACUUM

-- Vacuum: recupera espaço de linhas mortas
VACUUM;                          -- apenas marca espaço como reutilizável
VACUUM FULL;                     -- recupera espaço para o SO (bloqueia tabela!)
VACUUM ANALYZE;                  -- vacuum + atualiza estatísticas
ANALYZE;                         -- apenas atualiza estatísticas do planner

-- Verificar bloat de tabelas
SELECT
    schemaname,
    tablename,
    n_dead_tup,
    n_live_tup,
    round(n_dead_tup * 100.0 / NULLIF(n_live_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;

-- Configurar autovacuum por tabela
ALTER TABLE pedidos SET (
    autovacuum_vacuum_scale_factor = 0.01,       -- 1% de linhas mortas
    autovacuum_vacuum_threshold = 1000,           -- mínimo antes de vacuum
    autovacuum_analyze_scale_factor = 0.005,      -- 0.5% para analyze
    autovacuum_vacuum_cost_limit = 1000           -- mais agressivo
);

-- Monitorar autovacuum
SELECT
    relname,
    last_autovacuum,
    last_autoanalyze,
    n_dead_tup
FROM pg_stat_user_tables;

Lab: Análise de Performance com EXPLAIN

-- 03-performance-lab.sql
DROP TABLE IF EXISTS test_perf CASCADE;
CREATE TABLE test_perf AS
SELECT
    generate_series(1, 1000000) AS id,
    md5(random()::TEXT) AS hash,
    random() * 1000 AS valor,
    NOW() - (random() * 365 || ' days')::INTERVAL AS data,
    CASE WHEN random() < 0.5 THEN 'ativo' ELSE 'inativo' END AS status;

-- SEM ÍNDICE
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE id = 500000;
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE valor BETWEEN 100 AND 200;
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE status = 'ativo';

-- COM ÍNDICE B-tree
CREATE INDEX idx_perf_id ON test_perf(id);
CREATE INDEX idx_perf_valor ON test_perf(valor);
CREATE INDEX idx_perf_status ON test_perf(status) WHERE status = 'ativo';
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE id = 500000;
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE valor BETWEEN 100 AND 200;
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE status = 'ativo';

-- COM ÍNDICE BRIN (para dados ordenados)
CREATE INDEX idx_perf_data_brin ON test_perf USING BRIN(data);
EXPLAIN ANALYZE SELECT * FROM test_perf WHERE data > NOW() - INTERVAL '30 days';

-- VACUUM e estatísticas
VACUUM ANALYZE test_perf;
SELECT relname, n_live_tup, n_dead_tup FROM pg_stat_user_tables WHERE relname = 'test_perf';

-- Ver tamanho do índice
SELECT
    indexrelid::regclass AS index_name,
    pg_size_pretty(pg_relation_size(indexrelid::regclass)) AS index_size
FROM pg_stat_user_indexes
WHERE relname = 'test_perf';

Escolha o tipo de índice certo: B-tree para range/igualdade, GIN para JSONB/arrays, GiST para geometria/range, BRIN para logs. Partial indexes reduzem tamanho. Covering indexes (INCLUDE) habilitam index-only scans. EXPLAIN ANALYZE mostra o plano real.