Sobre nós Guias Projetos Contactos
Админка
please wait

Introdução

A otimização do desempenho do PostgreSQL é essencial para aplicações à escala. Consultas lentas, índices em falta e uma configuração inadequada podem comprometer seriamente o desempenho da aplicação. Engenheiros seniores devem compreender o planeamento de consultas, estratégias de indexação e afinação da configuração para manter sistemas responsivos.

Analisar o Desempenho das Consultas

EXPLAIN ANALYZE

EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 123
AND created_at > '2024-01-01'
ORDER BY created_at DESC
LIMIT 10;

Interpretação do output:

  • Seq Scan: Varredura completa da tabela (frequentemente mau para tabelas grandes)
  • Index Scan: Utilização de índice (bom)
  • Index Only Scan: Melhor — dados apenas a partir do índice
  • Bitmap Heap Scan: Combina múltiplas condições de índice
  • actual time: Tempo real de execução
  • rows: Linhas efetivamente processadas

Identificar Consultas Lentas

-- Ativar o registo de consultas lentas em postgresql.conf
-- log_min_duration_statement = 1000 -- Registar consultas > 1 segundo
-- Encontrar consultas atualmente em execução
SELECT pid, now() - query_start AS duration, state, query
FROM pg_stat_activity
WHERE state <> 'idle'
AND query NOT LIKE '% FROM pg_stat_activity %'
ORDER BY duration DESC;
-- Encontrar consultas de longa duração
SELECT datname, pid, state, query, age(clock_timestamp(), query_start) AS age
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY age DESC;

Terminar Consultas de Longa Duração

-- Cancelar consulta (de forma graciosa)
SELECT pg_cancel_backend(pid);
-- Terminar ligação (forçado)
SELECT pg_terminate_backend(pid);

Estratégias de Indexação

Índices Básicos

-- Índice de coluna única
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- Índice composto (a ordem das colunas importa!)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Índice único
CREATE UNIQUE INDEX idx_users_email ON users(email);

Índices Parciais

Indexar apenas as linhas que correspondem a uma condição:

-- Indexar apenas utilizadores ativos
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
-- Indexar apenas encomendas recentes
CREATE INDEX idx_recent_orders ON orders(user_id, created_at)
WHERE created_at > '2024-01-01';

Índices de Expressão

-- Índice em email em minúsculas
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- Índice no ano extraído
CREATE INDEX idx_orders_year ON orders(EXTRACT(YEAR FROM created_at));

Índices de Cobertura (INCLUDE)

Incluir colunas não indexadas para permitir index-only scans:

CREATE INDEX idx_orders_user ON orders(user_id)
INCLUDE (total, status, created_at);
-- A consulta pode agora ser satisfeita inteiramente a partir do índice
SELECT total, status, created_at FROM orders WHERE user_id = 123;

Quando Usar Diferentes Tipos de Índice

Tipo de ÍndiceCaso de Uso
B-treePredefinição, consultas de igualdade e de intervalo
HashApenas igualdade (raramente melhor do que B-tree)
GINPesquisa full-text, JSONB, arrays
GiSTDados geométricos, pesquisa full-text
BRINTabelas grandes com ordenação natural

Indexação de JSONB

-- Índice GIN para consultas de contenção JSONB
CREATE INDEX idx_products_metadata ON products USING gin(metadata);
-- Consulta: WHERE metadata @> '{"category": "electronics"}'
-- Indexar um caminho JSONB específico
CREATE INDEX idx_products_category ON products((metadata->>'category'));

Otimização de Consultas

Evitar SELECT *

-- Mau
SELECT * FROM orders WHERE user_id = 123;
-- Bom
SELECT id, total, status, created_at FROM orders WHERE user_id = 123;

Usar LIMIT com ORDER BY

-- Mau: Ordena todas as linhas
SELECT * FROM orders ORDER BY created_at DESC;
-- Bom: Para cedo com índice
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;

Paginação Eficiente

-- Mau: OFFSET varre e descarta linhas
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 10000;
-- Bom: Paginação por keyset
SELECT * FROM orders
WHERE id < 12345 -- Último ID da página anterior
ORDER BY id DESC
LIMIT 20;

Operações em Lote

-- Mau: Muitas transações pequenas
INSERT INTO logs (message) VALUES ('log 1');
INSERT INTO logs (message) VALUES ('log 2');
-- ... mais 1000
-- Bom: Uma única transação com múltiplas linhas
INSERT INTO logs (message) VALUES
('log 1'), ('log 2'), ('log 3'), ...;
-- Ou usar COPY para carregamentos em lote
COPY logs(message) FROM '/path/to/data.csv' CSV;

Evitar N+1 no Código da Aplicação

-- Em vez de consultar utilizadores e depois encomendas separadamente,
-- usar JOIN ou LATERAL JOIN
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id IN (1, 2, 3);

Otimização de PostGIS

Consultas de Geometria

-- Armazenar ponto
INSERT INTO locations (name, coordinates)
VALUES ('Office', ST_SetSRID(ST_MakePoint(-73.935242, 40.730610), 4326));
-- Consultar como GeoJSON
SELECT ST_AsGeoJSON(ST_Transform(coordinates, 3857))
FROM locations;
-- Encontrar pontos dentro do raio
SELECT * FROM locations
WHERE ST_DWithin(
coordinates::geography,
ST_MakePoint(-73.935, 40.730)::geography,
1000 -- metros
);
-- Índice espacial (essencial!)
CREATE INDEX idx_locations_geo ON locations USING gist(coordinates);

Afinação de Configuração

Definições de Memória

# postgresql.conf
# Memória partilhada para caching (25% da RAM)
shared_buffers = 4GB
# Memória por operação (sort, hash)
work_mem = 256MB
# Memória para operações de manutenção
maintenance_work_mem = 1GB
# Estimativa do query planner da cache
effective_cache_size = 12GB

Definições de Ligações

# Máximo de ligações
max_connections = 200
# Pooling de ligações (usar PgBouncer para melhor escalabilidade)

Desempenho de Escrita

# Definições de WAL para cargas de trabalho com muitas escritas
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 4GB

Manutenção de Tabelas

VACUUM e ANALYZE

-- Atualizar estatísticas (ajuda o query planner)
ANALYZE orders;
-- Recuperar espaço de linhas eliminadas
VACUUM orders;
-- Limpeza completa (bloqueia a tabela)
VACUUM FULL orders;
-- Definições de autovacuum
-- autovacuum = on (predefinição)

REINDEX

-- Reconstruir índice inchado
REINDEX INDEX idx_orders_user_id;
-- Reconstruir todos os índices na tabela
REINDEX TABLE orders;
-- Concurrently (sem lock, PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY idx_orders_user_id;

Estatísticas da Tabela

-- Verificar o tamanho da tabela
SELECT pg_size_pretty(pg_total_relation_size('orders'));
-- Verificar o tamanho do índice
SELECT pg_size_pretty(pg_relation_size('idx_orders_user_id'));
-- Estimativa de bloat da tabela
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;

Pooling de Ligações

Configuração do PgBouncer

# pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

Monitorização

Consultas Essenciais

-- Estatísticas de utilização de índices
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;
-- Índices não utilizados (candidatos a remoção)
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public';
-- Padrões de acesso à tabela
SELECT schemaname, relname, seq_scan, idx_scan,
n_tup_ins, n_tup_upd, n_tup_del
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
-- Taxa de acertos na cache (deve ser > 99%)
SELECT
sum(heap_blks_read) as heap_read,
sum(heap_blks_hit) as heap_hit,
sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio
FROM pg_statio_user_tables;

Manipulação de JSONB

Renomear Propriedades de Campos JSON

-- Exemplo: Renomear «wrongname» para «name» em JSONB aninhado
UPDATE products AS p
SET metadata = (
jsonb_set(
p.metadata::jsonb #- '{objectfield,wrongname}',
'{objectfield,name}',
p.metadata::jsonb #> '{objectfield,wrongname}'
)
)::json
WHERE p.metadata::jsonb ? 'objectfield'
AND p.metadata::jsonb #> '{objectfield}' ? 'wrongname';

Consultar Arrays JSONB

-- Encontrar itens em que o array contém um valor específico
SELECT * FROM products
WHERE metadata->'tags' ? 'featured';
-- Consultar elementos de array aninhados
SELECT * FROM products
WHERE metadata @> '{"categories": [{"name": "electronics"}]}';
-- Agregar elementos de array JSONB
SELECT id, jsonb_array_elements(metadata->'variants') as variant
FROM products;

Anonimização de Dados

Para ambientes de desenvolvimento/staging, anonimizar dados de produção:

-- Anonimizar dados de utilizadores para uso não produtivo
UPDATE users SET
first_name = SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(3*RANDOM())::int),
last_name = SUBSTR(MD5(RANDOM()::text), 1, 4 + FLOOR(4*RANDOM())::int),
address = CONCAT(
SUBSTR(MD5(RANDOM()::text), 1, 5 + FLOOR(5*RANDOM())::int), ' ',
SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(2*RANDOM())::int), ', ',
FLOOR(100*RANDOM())::int, ', ',
SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(3*RANDOM())::int)
),
email = CONCAT(
SUBSTR(MD5(RANDOM()::text), 1, 5 + FLOOR(5*RANDOM())::int), '@',
SUBSTR(MD5(RANDOM()::text), 1, 2 + FLOOR(3*RANDOM())::int), '.test'
),
phone = CONCAT('+1', FLOOR(1000000000 + 8999999999*RANDOM())::bigint);

Encontrar Tabelas Grandes

Identificar as tabelas que consomem mais armazenamento:

SELECT
schemaname AS schema,
tablename AS table,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS table_size,
pg_size_pretty(pg_indexes_size(schemaname || '.' || tablename)) AS indexes_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
LIMIT 20;

Gestão de Chaves Estrangeiras

Desativar Temporariamente para Operações em Lote

-- Desativar verificações de chaves estrangeiras (usar com cautela!)
SET session_replication_role = 'replica';
-- Executar operações em lote
TRUNCATE TABLE orders CASCADE;
-- Ou migrações de dados complexas...
-- Reativar verificações de chaves estrangeiras
SET session_replication_role = 'origin';

Checklist de Boas Práticas

  • [ ] Indexar colunas usadas em WHERE, JOIN, ORDER BY
  • [ ] Usar EXPLAIN ANALYZE para consultas lentas
  • [ ] Implementar pooling de ligações
  • [ ] Configurar shared_buffers de forma adequada
  • [ ] Executar ANALYZE após alterações de dados em lote
  • [ ] Monitorizar e remover índices não utilizados
  • [ ] Usar paginação por keyset para grandes conjuntos de dados
  • [ ] Manter as estatísticas atualizadas
  • [ ] Usar índices GIN para consultas JSONB
  • [ ] Anonimizar dados em ambientes não produtivos

Conclusão

A otimização do desempenho do PostgreSQL requer compreender a execução de consultas, indexação adequada e afinação da configuração. Comece com EXPLAIN ANALYZE para identificar estrangulamentos, adicione os índices apropriados e afine a configuração com base na carga de trabalho. A monitorização regular garante que o desempenho não se degrada ao longo do tempo.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive