CTO no Integrar Plus • Publicado em 12 de October de 2026
Toda vez que uma consulta em PostgreSQL começa a demorar centenas de milissegundos em produção, a reação instintiva da engenharia se divide em dois campos opostos e igualmente barulhentos. De um lado, estão os que cravam que o desenvolvedor não sabe escrever SQL, que o schema foi desenhado sem pensar no motor relacional e que a solução é entupir a tabela de índices compostos ou enfiar Redis na frente. Do outro, os que acusam o banco de ser uma tecnologia arcaica que deveria ser substituída por soluções customizadas ou hardware exótico.
Essas duas posturas erram o alvo com frequência porque ignoram a física do ambiente onde o banco roda. Há uma diferença brutal entre um plano de execução ineficiente que varre páginas demais na memória e o colapso silencioso do storage gerenciado em nuvem, onde o teto artificial de IOPS e a latência de rede transformam cada leitura em disco num gargalo intransponível.
O PostgreSQL foi desenhado sob a premissa de que páginas de 8 KB residem no buffer pool do sistema operacional e no shared_buffers do processo. Quando uma leitura precisa sair dessa camada de memória e buscar dados persistidos, o comportamento varia de forma drástica dependendo da topologia de hardware.
Em servidores dedicados ou VPS com NVMe local conectado diretamente ao barramento PCIe, uma leitura aleatória leva cerca de 20 a 50 microssegundos. Já em volumes de bloco anexados por rede, como os discos gerenciados padrão dos grandes provedores de nuvem, essa mesma operação física raramente custa menos de 1 a 3 milissegundos. Estamos falando de uma penalidade de duas ordens de magnitude por bloco lido.
Se uma consulta precisa ler 500 páginas fora do cache:
Criar um índice novo para transformar um Seq Scan em um Index Scan pode reduzir o volume bruto de dados avaliados, mas se o índice for grande o bastante para não caber no cache, cada salto na árvore B-Tree exige travessias adicionais que batem diretamente na rede do storage. O resultado prático é que o índice novo pode inclusive piorar a latência da consulta sob carga concorrente.
O detalhe perverso dos discos de nuvem gerenciados é o modelo de cobrança baseado em IOPS provisionados e créditos de rajada (burst buckets). Enquanto a aplicação tem pouco tráfego, o volume consome créditos acumulados e entrega latências aceitáveis. Quando a carga sobe e a cota se esgota, o provedor impõe throttling imediato.
A partir desse momento, as requisições de I/O entram em filas no driver do kernel da máquina virtual. O sintoma visível no PostgreSQL não é erro de sintaxe ou crash; é uma explosão súbita em métricas como iowait no sistema operacional e eventos de espera do tipo IO:DataFileRead ou IO:WALWrite no próprio banco.
Se a equipe analisa apenas o tempo total da query sem observar os eventos de espera, conclui erroneamente que o plano de consulta degradou. Começam as tentativas desesperadas de adicionar índices, reescrever joins ou forçar hints, enquanto o problema real é que o storage simplesmente se recusa a entregar mais do que a cota contratada permite.
Para separar um problema algorítmico de saturação física de infraestrutura, a saída padrão do EXPLAIN ANALYZE precisa ser enriquecida com o parâmetro de buffers:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, updated_at
FROM pedidos
WHERE cliente_id = 48291
ORDER BY criado_em DESC
LIMIT 50;
A inspeção desses blocos revela a verdade:
Limit (cost=0.42..15.20 rows=50 width=24) (actual time=1.820..420.150 rows=50 loops=1)
Buffers: shared hit=12 read=380
-> Index Scan using idx_pedidos_cliente on pedidos (cost=0.42..295.10 rows=1000 width=24) (actual time=1.815..420.130 rows=50 loops=1)
Index Cond: (cliente_id = 48291)
Buffers: shared hit=12 read=380
Planning Time: 0.120 ms
Execution Time: 420.210 ms
No exemplo acima, a consulta executou um Index Scan, comprovando que o planejador encontrou e usou o índice correto. No entanto, foram necessárias 380 leituras reais (shared read=380) contra apenas 12 acertos em memória (shared hit=12).
Se a execução levou 420 milissegundos para ler apenas 380 blocos, isso significa uma média superior a 1 milissegundo por bloco de 8 KB. O gargalo não é o índice nem o SQL: é a latência de entrega do disco.
Para confirmar se o storage está sofrendo contenção no nível do sistema operacional, ferramentas simples como iostat mostram a fila de espera e o tempo de serviço:
iostat -xz 1
Foque em duas colunas cruciais:
await ou r_await: tempo médio em milissegundos que as requisições de leitura levaram desde o envio até o atendimento. Números acima de 2 a 5 ms em volumes SSD indicam contenção de rede ou estrangulamento do provedor.%util: percentual de tempo em que o dispositivo esteve ocupado. Em discos virtuais de nuvem, esse valor pode bater 100% mesmo com baixa vazão em MB/s se o limite de operações por segundo tiver sido atingido.Dentro da engine, a visão pg_stat_activity expõe o estado das conexões travadas:
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE state = 'active'
GROUP BY 1, 2
ORDER BY 3 DESC;
Se a maioria das sessões ativas estiver acumulada em wait_event_type = 'IO', qualquer esforço de refatoração de código na aplicação terá impacto marginal até que o problema de I/O seja resolvido.
Muitos engenheiros defendem o armazenamento de bloco gerenciado alegando facilidade de snapshots, migração a quente e redimensionamento sem desligar a máquina. O trade-off raramente admitido é que essa comodidade custa caro e cobra um pedágio permanente em cada operação transacional síncrona, principalmente durante a escrita do Write-Ahead Log (WAL).
Cada COMMIT exige a descarga forçada (fsync) do log no disco. Em volumes remotos, cada commit espera pelo ciclo completo de ida e volta da rede até o storage do provedor confirmar a persistência. Quando a taxa de escrita aumenta, o gargalo se agrava exponencialmente.
Em arquiteturas que rodam sobre bare metal ou instâncias com discos NVMe locais, a latência de fsync cai para frações de milissegundo. A flexibilidade operacional que a nuvem vende com seus volumes de rede pode ser compensada de forma mais eficiente com réplicas de leitura em streaming contínuo e ferramentas maduras de backup físico como pgBackRest ou WAL-G, que enviam dados para armazenamento de objetos de forma assíncrona sem comprometer o caminho crítico da transação.
Antes de pagar o triplo por classes mais caras de IOPS provisionados ou reescrever camadas inteiras da aplicação:
shared_buffers e no cache do sistema, você sempre estará à mercê do disco. Aumentar a memória costuma ser mais barato do que tentar compensar I/O ruim com IOPS provisionados.INSERT, UPDATE e DELETE, consumindo a cota de I/O que as leituras precisam.CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.