| name | sql-optimization-patterns |
| description | Define estratégias de otimização de consultas e uso de índices em bancos relacionais. Use ao analisar queries com EXPLAIN, investigar lentidão no banco ou otimizar a performance de acesso a dados. |
Padrões de Otimização SQL (SQL Optimization Patterns)
Transforme consultas de banco de dados lentas em operações ultrarrápidas através de otimização sistemática, indexação correta e análise de plano de execução (query plan).
Quando Usar Esta Skill
- Debugar queries lentas
- Projetar schemas de banco de dados performáticos
- Otimizar tempos de resposta da aplicação
- Reduzir carga e custos do banco de dados
- Melhorar escalabilidade para datasets em crescimento
- Analisar planos de consulta EXPLAIN
- Implementar índices eficientes
- Resolver problemas de N+1 queries
Conceitos Principais (Core Concepts)
1. Planos de Execução de Queries (Query Execution Plans - EXPLAIN)
Entender o output do EXPLAIN é fundamental para otimização.
PostgreSQL EXPLAIN:
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT u.*, o.order_total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '30 days';
Métricas Chave a Observar:
- Seq Scan: Scan completo da tabela (geralmente lento para tabelas grandes)
- Index Scan: Usando índice (bom)
- Index Only Scan: Usando índice sem tocar na tabela (melhor)
- Nested Loop: Método de Join (ok para pequenos datasets)
- Hash Join: Método de Join (bom para grandes datasets)
- Merge Join: Método de Join (bom para dados ordenados)
- Cost: Custo estimado da query (quanto menor, melhor)
- Rows: Linhas estimadas retornadas
- Actual Time: Tempo de execução real
2. Estratégias de Índices
Índices são a ferramenta de otimização mais poderosa.
Tipos de Índice:
- B-Tree: Padrão, bom para consultas de igualdade e range (intervalo)
- Hash: Apenas para comparações de igualdade (=)
- GIN: Busca de texto completo (full-text search), queries de array, JSONB
- GiST: Dados geométricos, full-text search
- BRIN: Block Range INdex para tabelas muito grandes com correlação
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
CREATE INDEX idx_users_email_covering ON users(email)
INCLUDE (name, created_at);
CREATE INDEX idx_posts_search ON posts
USING GIN(to_tsvector('english', title || ' ' || body));
CREATE INDEX idx_metadata ON events USING GIN(metadata);
3. Padrões de Otimização de Consultas (Query Optimization Patterns)
Evite SELECT *:
SELECT * FROM users WHERE id = 123;
SELECT id, email, name FROM users WHERE id = 123;
Use a Cláusula WHERE de Forma Eficiente:
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
SELECT * FROM users WHERE email = 'user@example.com';
Otimize JOINs:
SELECT u.name, o.total
FROM users u, orders o
WHERE u.id = o.user_id AND u.created_at > '2024-01-01';
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01';
SELECT u.name, o.total
FROM (SELECT * FROM users WHERE created_at > '2024-01-01') u
JOIN orders o ON u.id = o.user_id;
Padrões de Otimização
Padrão 1: Eliminar Consultas N+1 (Eliminate N+1 Queries)
Problema: Anti-Padrão N+1 Query
users = db.query("SELECT * FROM users LIMIT 10")
for user in users:
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user.id)
Solução: Usar JOINs ou Carregamento em Lote (Batch Loading)
SELECT
u.id, u.name,
o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN (1, 2, 3, 4, 5);
SELECT * FROM orders
WHERE user_id IN (1, 2, 3, 4, 5);
results = db.query("""
SELECT u.id, u.name, o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN (1, 2, 3, 4, 5)
""")
users = db.query("SELECT * FROM users LIMIT 10")
user_ids = [u.id for u in users]
orders = db.query(
"SELECT * FROM orders WHERE user_id IN (?)",
user_ids
)
orders_by_user = {}
for order in orders:
orders_by_user.setdefault(order.user_id, []).append(order)
Padrão 2: Otimizar Paginação
Ruim: OFFSET em Tabelas Grandes
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 100000;
Bom: Paginação Baseada em Cursor (Cursor-Based Pagination)
SELECT * FROM users
WHERE created_at < '2024-01-15 10:30:00'
ORDER BY created_at DESC
LIMIT 20;
SELECT * FROM users
WHERE (created_at, id) < ('2024-01-15 10:30:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 20;
CREATE INDEX idx_users_cursor ON users(created_at DESC, id DESC);
Padrão 3: Agrupar Eficientemente (Aggregate Efficiently)
Otimizar Consultas COUNT:
SELECT COUNT(*) FROM orders;
SELECT reltuples::bigint AS estimate
FROM pg_class
WHERE relname = 'orders';
SELECT COUNT(*) FROM orders
WHERE created_at > NOW() - INTERVAL '7 days';
CREATE INDEX idx_orders_created ON orders(created_at);
SELECT COUNT(*) FROM orders
WHERE created_at > NOW() - INTERVAL '7 days';
Otimizar GROUP BY:
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 10;
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE status = 'completed'
GROUP BY user_id
HAVING COUNT(*) > 10;
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
Padrão 4: Otimização de Subconsultas (Subquery Optimization)
Transformar Subconsultas Correlacionadas:
SELECT u.name, u.email,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count
FROM users u;
SELECT u.name, u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name, u.email;
SELECT DISTINCT ON (u.id)
u.name, u.email,
COUNT(o.id) OVER (PARTITION BY u.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
Use CTEs para Clareza:
WITH recent_users AS (
SELECT id, name, email
FROM users
WHERE created_at > NOW() - INTERVAL '30 days'
),
user_order_counts AS (
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY user_id
)
SELECT ru.name, ru.email, COALESCE(uoc.order_count, 0) as orders
FROM recent_users ru
LEFT JOIN user_order_counts uoc ON ru.id = uoc.user_id;
Padrão 5: Operações em Lote (Batch Operations)
Batch INSERT:
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
INSERT INTO users (name, email) VALUES ('Carol', 'carol@example.com');
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Carol', 'carol@example.com');
COPY users (name, email) FROM '/tmp/users.csv' CSV HEADER;
Batch UPDATE:
UPDATE users SET status = 'active' WHERE id = 1;
UPDATE users SET status = 'active' WHERE id = 2;
UPDATE users
SET status = 'active'
WHERE id IN (1, 2, 3, 4, 5, ...);
CREATE TEMP TABLE temp_user_updates (id INT, new_status TEXT);
INSERT INTO temp_user_updates VALUES (1, 'active'), (2, 'active'), ...;
UPDATE users u
SET status = t.new_status
FROM temp_user_updates t
WHERE u.id = t.id;
Técnicas Avançadas
Views Materializadas (Materialized Views)
Pré-compute queries caras.
CREATE MATERIALIZED VIEW user_order_summary AS
SELECT
u.id,
u.name,
COUNT(o.id) as total_orders,
SUM(o.total) as total_spent,
MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
CREATE INDEX idx_user_summary_spent ON user_order_summary(total_spent DESC);
REFRESH MATERIALIZED VIEW user_order_summary;
REFRESH MATERIALIZED VIEW CONCURRENTLY user_order_summary;
SELECT * FROM user_order_summary
WHERE total_spent > 1000
ORDER BY total_spent DESC;
Particionamento
Divida tabelas grandes para melhor performance.
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id INT,
total DECIMAL,
created_at TIMESTAMPTZ
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2024_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
SELECT * FROM orders
WHERE created_at BETWEEN '2024-02-01' AND '2024-02-28';
Ajustes de Planner e Diagnóstico (PostgreSQL)
PostgreSQL não possui hints nativas por query (como USE INDEX). Para investigação, faça ajustes locais de planner e valide com EXPLAIN ANALYZE.
BEGIN;
SET LOCAL max_parallel_workers_per_gather = 4;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM large_table WHERE condition;
ROLLBACK;
BEGIN;
SET LOCAL enable_nestloop = OFF;
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.id, o.id
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days';
ROLLBACK;
Melhores Práticas
- Indexe de Forma Seletiva: Muitos índices deixam os writes (escritas) mais lentos.
- Monitore a Performance das Consultas: Use slow query logs.
- Mantenha as Estatísticas Atualizadas: Rode
ANALYZE regularmente.
- Use os Tipos de Dados Adequados: Tipos menores = melhor performance.
- Normalize com Cuidado: Equilibre normalização vs performance.
- Faça Cache de Dados Muito Acessados: Use cache em nível de aplicação.
- Pool de Conexões (Connection Pooling): Reutilize conexões de banco de dados.
- Manutenção Regular:
VACUUM, ANALYZE, reconstruir índices.
ANALYZE users;
ANALYZE VERBOSE orders;
VACUUM ANALYZE users;
VACUUM FULL users;
REINDEX INDEX idx_users_email;
REINDEX TABLE users;
Armadilhas Comuns (Common Pitfalls)
- Excesso de Índices (Over-Indexing): Cada índice torna INSERT/UPDATE/DELETE mais lentos
- Índices Não Utilizados: Desperdiçam espaço e lentificam escritas
- Índices Faltando: Queries lentas, full table scans
- Conversão de Tipo Implícita: Previne o uso de índice
- Condições OR: Não conseguem usar índices de forma eficiente
- LIKE com Curinga no Começo (Leading Wildcard):
LIKE '%abc' não pode usar índice
- Função no WHERE: Previne o uso de índice a menos que exista um índice funcional correspondente
Monitoramento de Consultas
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan,
seq_tup_read / seq_scan AS avg_seq_tup_read
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 10;
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
Notas de 2026
pg_stat_statements exige shared_preload_libraries = 'pg_stat_statements' em postgresql.conf + restart; depois, habilite com CREATE EXTENSION pg_stat_statements e compute_query_id em auto/on.
- PostgreSQL não possui atualização incremental nativa (incremental refresh) de materialized views;
REFRESH substitui todo o conteúdo.
- Use
EXPLAIN (ANALYZE, BUFFERS, SETTINGS, WAL) para um diagnóstico mais completo possível (disponível em PostgreSQL 14+, que é a base do projeto).
Recursos Oficiais