PostgreSQL: tuning e investigação pelo console SQL
Série PostgreSQL — parte 2 de 2
- Instalação no Mac e bugs comuns
- Você está aqui — Tuning e investigação pelo console SQL
Com o Postgres rodando e o pg_stat_statements habilitado (ver parte 1), o passo seguinte é entender o que está lento e por quê. Esse post junta as queries de diagnóstico que eu rodo direto no psql quando preciso entender uma instalação que não conheço bem.
EXPLAIN visual com PEV2
EXPLAIN ANALYZE cuspido no terminal é legível, mas árvore com 50 nós vira sopa. Pra entender plano grande eu uso o PEV2 — basta gerar o plano em JSON e colar na página.
1
2
EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON)
SELECT ...;
ANALYZEexecuta a query de fato (cuidado comUPDATE/DELETE).BUFFERSmostra cache hit vs leitura de disco — fundamental pra saber se o problema é I/O ou plano.FORMAT JSONé o formato que o PEV2 entende.
Salve o JSON num arquivo, abra o PEV2 (dá pra rodar offline com o HTML que eles distribuem), cole, e você ganha um diagrama com tempos por nó e gargalos pintados.
Ajustando work_mem pra uma query
Se uma query está fazendo merge/sort em disco em vez de em memória, dá pra dar mais memória só pra ela:
1
2
3
SET work_mem = '100MB';
COMMIT;
SHOW work_mem;
SET só vale na sessão atual — sem risco de afetar produção globalmente. Útil pra testar se aumentar memória resolve antes de fazer alteração no postgresql.conf (que afeta todas as conexões e pode estourar RAM em paralelo).
Resetando estatísticas pra começar limpo
1
SELECT pg_stat_reset();
Zera os contadores de pg_stat_*. Útil quando você quer medir só o efeito de uma mudança recente — depois de rodar isso, espera um pouco e vai ver o estado novo do Postgres sem o histórico misturado.
Uso de índices: quem é útil e quem está lá à toa
Essa é a query que eu mais rodo em banco que herdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SELECT
idstat.relname AS table_name,
indexrelname AS index_name,
idstat.idx_scan AS index_scans_count,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
tabstat.idx_scan AS table_reads_index_count,
tabstat.seq_scan AS table_reads_seq_count,
tabstat.seq_scan + tabstat.idx_scan AS table_reads_count,
n_tup_upd + n_tup_ins + n_tup_del AS table_writes_count,
pg_size_pretty(pg_relation_size(idstat.relid)) AS table_size
FROM
pg_stat_user_indexes AS idstat
JOIN
pg_indexes
ON indexrelname = indexname
AND idstat.schemaname = pg_indexes.schemaname
JOIN
pg_stat_user_tables AS tabstat
ON idstat.relid = tabstat.relid
WHERE
indexdef !~* 'unique'
ORDER BY
idstat.idx_scan DESC,
pg_relation_size(indexrelid) DESC;
A leitura é direta:
- Índice com
index_scans_count= 0 e tamanho grande é candidato aDROP INDEX. Está ocupando disco e desacelerando todoINSERT/UPDATEdaquela tabela sem ser usado. - Tabela com
seq_scanmuito maior queidx_scanem geral está faltando índice — ou tem cardinalidade tão baixa queseq_scané melhor mesmo (medir antes de criar). - O filtro
indexdef !~* 'unique'esconde índices únicos (que existem por garantia de constraint, não por performance) — eles não devem ser candidatos a remoção.
Contar linhas de todas as tabelas de um schema
SELECT count(*) FROM cada_tabela é tedioso. Essa query gera o count(*) por tabela usando XML pra evitar SQL dinâmico:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT table_schema,
table_name,
(xpath('/row/cnt/text()', xml_count))[1]::text::int AS row_count
FROM (
SELECT table_name,
table_schema,
query_to_xml(
format('SELECT count(*) AS cnt FROM %I.%I', table_schema, table_name),
false, true, ''
) AS xml_count
FROM information_schema.tables
WHERE table_schema = 'public'
) t
ORDER BY table_name;
Troque 'public' pelo schema que você quer inspecionar. É lento em base grande (faz count(*) sequencial em cada tabela), mas dá um inventário completo numa query só.
Achar uma foreign key pelo nome
Quando o Rails reclama PG::ForeignKeyViolation: ERROR: insert or update on table "x" violates foreign key constraint "fk_rails_96f00dec22", o nome fk_rails_<hash> não diz nada. Pra descobrir o que ela referencia:
1
2
3
4
5
6
7
8
SELECT conrelid::regclass AS table_name,
conname AS foreign_key,
pg_get_constraintdef(oid)
FROM pg_constraint
WHERE contype = 'f'
AND conname = 'fk_rails_96f00dec22'
AND connamespace = 'public'::regnamespace
ORDER BY conrelid::regclass::text, contype DESC;
pg_get_constraintdef cospe o FOREIGN KEY (...) REFERENCES ... em texto. Junto com conrelid::regclass (a tabela onde a FK vive), você fecha a investigação em segundos.
Resetando uma sequence depois de import manual
Se você importou dados via INSERT mantendo os IDs originais, a sequence não foi tocada — o próximo INSERT que dependa do nextval vai estourar duplicate key. Pra ressincronizar:
1
SELECT SETVAL('table_name_id_seq', (SELECT MAX(id) + 1 FROM table_name));
Convenção do Postgres: a sequence default de tabela.id se chama tabela_id_seq. Se você renomeou o PK ou usou nome custom, confira no \d table_name.
Bônus: dump pra arquivo
Não é tuning, mas é o comando que sempre uso junto:
1
pg_dump -C -h localhost -U user dbname > ~/source/db1.sql
-C inclui o CREATE DATABASE no dump — útil pra restaurar num servidor onde o banco ainda não existe. Sem -C, o dump assume que o banco-destino já está criado.
Próximas anotações
- Receita pra caçar slow query com
pg_stat_statements(top 20 portotal_time, top 20 porcalls). - Configuração inicial de
shared_buffers,effective_cache_sizeerandom_page_costpra SSD. VACUUM,ANALYZEe quando rodar manualmente além do autovacuum.