Ajuste de desempenho Go + Postgres: um manual focado para APIs
Guia de otimização Go + Postgres para APIs geradas por IA: ajustar pool, ler planos EXPLAIN, indexar com sabedoria, paginar com segurança e enxugar JSON.

Como “lento” se manifesta em APIs Go com Postgres
APIs geradas por IA podem parecer rápidas nos primeiros testes. Você chama um endpoint algumas vezes, o conjunto de dados é pequeno e as requisições chegam uma de cada vez. Então vem tráfego real: endpoints mistos, carga em rajadas, caches mais frios e mais linhas do que você esperava. O mesmo código pode começar a ficar aleatoriamente lento mesmo que nada tenha quebrado.
O lento geralmente aparece de algumas formas: picos de latência (a maioria das requisições vai bem, mas algumas demoram de 5x a 50x mais), timeouts (uma pequena porcentagem falha) ou CPU alta (CPU do Postgres por trabalho de query, ou CPU do Go por JSON, goroutines, logging e retries).
Um cenário comum é um endpoint de listagem com um filtro flexível que retorna um grande JSON. Em um banco de testes, ele escaneia alguns milhares de linhas e termina rápido. Em produção, ele escaneia alguns milhões, ordena e só então aplica um LIMIT. A API ainda “funciona”, mas a latência p95 explode e algumas requisições dão timeout durante picos.
Para separar lentidão do banco da lentidão do app, mantenha o modelo mental simples.
Se o banco está lento, seu handler Go passa a maior parte do tempo esperando a query. Você também pode ver muitas requisições “em voo” enquanto a CPU do Go parece normal.
Se o app está lento, a query termina rápido, mas o tempo se perde depois: construindo objetos grandes de resposta, serializando JSON, rodando queries extras por linha ou fazendo trabalho demais por requisição. CPU e memória do Go sobem, e a latência cresce com o tamanho da resposta.
“Bom o suficiente” antes do lançamento não é perfeição. Para muitos endpoints CRUD, mire em p95 estável (não só a média), comportamento previsível sob rajadas e sem timeouts no pico esperado. O objetivo é simples: sem requisições lentas-surpresa quando dados e tráfego crescem, e sinais claros quando algo muda.
Linha de base primeiro: os poucos números que importam
Antes de otimizar qualquer coisa, decida o que “bom” significa para sua API. Sem uma linha de base, é fácil passar horas mudando configurações e ainda não saber se melhorou ou só mudou o gargalo.
Três números geralmente contam a maior parte da história:
- latência p95 (não média)
- taxa de erro (HTTP 5xx, timeouts, requisições canceladas)
- tempo de BD por requisição (quanto cada requisição espera no Postgres)
p95 é a métrica do “dia ruim”. Se p95 é alto mas a média vai bem, um pequeno conjunto de requisições está fazendo trabalho demais, sendo bloqueado por locks ou acionando planos lentos.
Torne queries lentas visíveis cedo. No Postgres, ative o log de queries lentas com um limiar baixo para testes pré-lançamento (por exemplo, 100–200 ms) e registre a declaração completa para poder copiá-la num cliente SQL. Mantenha isso temporário. Logar toda query lenta em produção vira ruído rápido.
Depois, teste com requisições que pareçam reais, não apenas uma rota "hello world". Um pequeno conjunto basta se combinar com o que os usuários realmente vão fazer: uma chamada de listagem com filtros e ordenação, uma página de detalhe com alguns joins, um create/update com validação e uma query estilo busca com matches parciais.
Se você gera endpoints a partir de uma especificação (por exemplo, com uma ferramenta de vibe-coding como Koder.ai), execute o mesmo punhado de requisições repetidamente com entradas consistentes. Isso torna mudanças como índices, ajustes de paginação e reescritas de query mais fáceis de medir.
Por fim, escolha um alvo que você consiga dizer em voz alta. Exemplo: “A maioria das requisições fica abaixo de 200 ms p95 com 50 usuários concorrentes, e erros abaixo de 0,5%.” Os números exatos dependem do produto, mas um objetivo claro evita mexer sem fim.
Pool de conexões que mantém o Postgres estável
Um pool de conexões mantém um número limitado de conexões abertas ao banco e as reutiliza. Sem pool, cada requisição pode abrir uma nova conexão, e o Postgres perde tempo e memória gerenciando sessões em vez de rodar queries.
O objetivo é manter o Postgres ocupado fazendo trabalho útil, não trocando contexto entre conexões demais. Esse é frequentemente o primeiro ganho real, especialmente para APIs geradas por IA que silenciosamente viram endpoints “tagarelas”.
Configurações simples de início
Em Go, você geralmente ajusta max open connections, max idle connections e lifetime das conexões. Um ponto de partida seguro para muitas APIs pequenas é um pequeno múltiplo dos núcleos de CPU (frequentemente 5 a 20 conexões no total), com número similar mantido ocioso, e reciclando conexões periodicamente (por exemplo, a cada 30 a 60 minutos).
Se você roda múltiplas instâncias da API, lembre que o pool se multiplica. Um pool de 20 conexões em 10 instâncias é 200 conexões batendo no Postgres — e é assim que times inesperadamente batem nos limites de conexão.
Como saber se o pool é o problema
Problemas de pool se sentem diferentes de SQL lento.
Se o pool for pequeno demais, requisições esperam antes de chegar ao Postgres. Latência sobe, mas CPU do banco e tempos de query podem parecer normais.
Se o pool for grande demais, o Postgres fica sobrecarregado: muitas sessões ativas, pressão de memória e latência desigual entre endpoints.
Uma forma rápida de separar é cronometrar suas chamadas ao BD em duas partes: tempo esperando pela conexão vs tempo executando a query. Se a maior parte do tempo é “espera”, o pool é o gargalo. Se a maior parte é “na query”, foque em SQL e índices.
Checagens rápidas úteis:
- Logue estatísticas do pool (open, in-use, idle) e observe se in-use fica preso no máximo.
- Adicione um timeout no acquire da conexão para que esperas falhem rápido em staging.
- Monitore conexões ativas no Postgres e quão perto você está de
max_connections. - Confirme que cada requisição fecha rows e libera conexões prontamente.
- Faça load test com o mesmo número de instâncias de app que planeja rodar.
pgxpool vs database/sql
Se você usa pgxpool, tem um pool orientado ao Postgres com estatísticas claras e bons defaults para comportamento do Postgres. Se usa database/sql, tem uma interface padrão que funciona entre bancos, mas precisa ser explícito sobre configurações de pool e comportamento do driver.
Uma regra prática: se você está 100% em Postgres e quer controle direto, pgxpool costuma ser mais simples. Se depende de bibliotecas que esperam database/sql, fique com ele, configure o pool explicitamente e meça esperas.
Exemplo: um endpoint de listagem de pedidos pode rodar em 20 ms, mas com 100 usuários concorrentes pula para 2 s. Se logs mostram 1,9 s esperando por conexão, otimizar query não vai ajudar até que o pool e as conexões totais do Postgres sejam dimensionadas corretamente.
Planejamento de consulta: leituras rápidas do EXPLAIN
Quando um endpoint parece lento, verifique o que o Postgres está realmente fazendo. Uma leitura rápida de EXPLAIN frequentemente aponta a correção em minutos.
Execute isso no SQL exato que sua API envia:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, status, created_at
FROM orders
WHERE user_id = $1 AND status = $2
ORDER BY created_at DESC
LIMIT 50;
Algumas linhas importam mais. Veja o nó de topo (o que o Postgres escolheu) e os totais no final (quanto tempo levou). Compare estimado vs real. Grandes diferenças geralmente significam que o planner errou.
O que as linhas-chave geralmente significam
Se você vê Index Scan ou Index Only Scan, o Postgres está usando um índice, o que geralmente é bom. Bitmap Heap Scan pode ser aceitável para matches de tamanho médio. Seq Scan significa que leu a tabela inteira, o que só é OK quando a tabela é pequena ou quase todas as linhas casam.
Sinais de alerta comuns:
- Seq Scan em tabela grande
- Estimativa de linhas muito diferente do real (por exemplo, 10 estimadas vs 10.000 reais)
- Sort consumindo a maior parte do tempo (frequentemente pareado com
ORDER BY) - “Filter:” removendo muitas linhas depois do scan
- Muitos shared read blocks em BUFFERS (muito dado lido)
Por que planos falham (e correções fáceis)
Planos lentos normalmente vêm de alguns padrões:
- Índice faltando para seu padrão
WHERE+ORDER BY(por exemplo,(user_id, status, created_at)) - Tipos incompatíveis (por exemplo, comparando uma coluna UUID com um parâmetro text), que podem impedir uso de índice
- Funções no
WHERE(por exemplo,WHERE lower(email) = $1), que podem forçar scans a menos que você adicione um índice de expressão correspondente
Se o plano parece estranho e as estimativas estão bem erradas, as estatísticas frequentemente estão desatualizadas. Rode ANALYZE (ou deixe o autovacuum atualizar) para que o Postgres aprenda contagens atuais e distribuições de valores. Isso importa após grandes importações ou quando novos endpoints começam a escrever muitos dados rapidamente.
Indexação para as queries que você realmente roda
Índices ajudam apenas quando batem com como sua API consulta dados. Se você os cria a partir de suposições, acaba com writes mais lentos, armazenamento maior e pouco ou nenhum ganho.
Uma forma prática de pensar: um índice é um atalho para uma pergunta específica. Se sua API faz outra pergunta, o Postgres ignora o atalho.
Construa índices ao redor de filtros + ordem
Se um endpoint filtra por account_id e ordena por created_at DESC, um índice composto frequentemente vence dois índices separados. Ele ajuda o Postgres a encontrar as linhas certas e retorná-las na ordem certa com menos trabalho.
Regras práticas:
- Indexe as colunas que você filtra mais, depois adicione a coluna pela qual ordena.
- Mantenha índices compostos pequenos. Duas colunas é comum; três às vezes ok; mais geralmente é sinal de problema.
- Coloque primeiro o filtro mais seletivo (o que reduz mais os resultados).
- Evite índices separados que são totalmente cobertos por um índice composto melhor.
- Prefira um índice bem escolhido em vez de vários “talvez úteis”.
Exemplo: se sua API tem GET /orders?status=paid e sempre mostra os mais novos primeiro, um índice como (status, created_at DESC) é um bom ajuste. Se a maioria das queries também filtra por cliente, (customer_id, status, created_at) pode ser melhor, mas só se for assim que o endpoint roda em produção.
Índices parciais para filtros comuns
Se a maior parte do tráfego atinge uma fatia estreita de linhas, um índice parcial pode ser mais barato e rápido. Por exemplo, se seu app lê principalmente registros ativos, indexar apenas WHERE active = true mantém o índice menor e mais provável de ficar em memória.
Para confirmar que um índice ajuda, faça verificações rápidas:
- Rode
EXPLAIN(ouEXPLAIN ANALYZEem ambiente seguro) e procure por um index scan que combine com sua query. - Compare tempo e linhas lidas com e sem o índice.
- Observe “Rows Removed by Filter” permanecendo alto. Isso costuma indicar que o índice não corresponde ao seu filtro.
Remova índices não usados com cuidado. Verifique estatísticas de uso (por exemplo, se um índice foi escaneado). Apague um por vez em janelas de baixo risco e mantenha um plano de rollback. Índices não usados não são inofensivos. Eles tornam inserts e updates mais lentos em cada escrita.
Padrões de paginação que não degradam com o tempo
A paginação é muitas vezes onde uma API rápida começa a parecer lenta, mesmo quando o banco está saudável. Trate paginação como um problema de design de query, não um detalhe de UI.
Por que LIMIT/OFFSET fica mais lento
LIMIT/OFFSET parece simples, mas páginas mais profundas costumam custar mais. O Postgres ainda precisa pular (e muitas vezes ordenar) as linhas que você está ignorando. A página 1 pode tocar algumas dezenas de linhas. A página 500 pode forçar o banco a escanear e descartar dezenas de milhares só para retornar 20 resultados.
Também pode criar resultados instáveis quando linhas são inseridas ou deletadas entre requisições. Usuários podem ver duplicatas ou perder itens porque o significado de “linha 10.000” muda conforme a tabela muda.
Paginação por keyset (cursor) com exemplo de “último visto”
A paginação por keyset faz uma pergunta diferente: “Me dê as próximas 20 linhas depois da última que vi.” Isso mantém o banco trabalhando em uma fatia pequena e consistente.
Uma versão simples usa um id crescente:
SELECT id, created_at, title
FROM posts
WHERE id > $1
ORDER BY id
LIMIT 20;
Sua API retorna um next_cursor igual ao último id da página. A próxima requisição usa esse valor como $1.
Para ordenação por tempo, use uma ordem estável e desempate. created_at sozinho não basta se duas linhas tiverem o mesmo timestamp. Use um cursor composto:
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Algumas regras evitam duplicatas e itens perdidos:
- Inclua sempre um desempate único no
ORDER BY(geralmenteid). - Mantenha a ordem de sort idêntica entre requisições.
- Torne o cursor opaco para clientes (codifique
created_ateidjuntos). - Se usuários puderem filtrar, inclua os mesmos filtros em cada página.
- Prefira campos de ordenação imutáveis (tempo de criação) em vez de mutáveis (status, score) quando possível.
Moldando JSON: respostas mais rápidas com payloads menores
Uma razão surpreendentemente comum para uma API parecer lenta não é o banco. É a resposta. JSON grande leva mais tempo para construir, mais tempo para enviar e mais tempo para o cliente parsear. A vitória mais rápida frequentemente é retornar menos.
Comece no seu SELECT. Se um endpoint precisa apenas de id, name e status, peça só essas colunas. SELECT * fica mais pesado silenciosamente conforme as tabelas ganham textos longos, blobs JSON e colunas de auditoria.
Outra lentidão frequente é construir respostas N+1: você busca uma lista de 50 itens e depois roda 50 queries a mais para anexar dados relacionados. Pode passar nos testes e colapsar com tráfego real. Prefira uma única query que retorne o que precisa (joins cuidadosos) ou duas queries onde a segunda faz batch por IDs.
Algumas maneiras de manter payloads menores sem quebrar clientes:
- Use uma flag
include=(ou uma máscarafields=) para que respostas de listagem fiquem enxutas e as de detalhe optem por extras. - Limite arrays aninhados (por exemplo, apenas os 10 eventos mais recentes) e ofereça endpoint separado para histórico completo.
- Não retorne colunas JSON internas brutas se o cliente precisa só de algumas chaves.
- Use códigos curtos em vez de repetir rótulos longos.
Construir JSON no Postgres ou no Go?
Ambos podem ser rápidos. Escolha com base no que você está otimizando.
Funções JSON do Postgres (jsonb_build_object, json_agg) são úteis quando você quer menos idas e vindas e formatos previsíveis de uma query. Moldar em Go é útil quando precisa de lógica condicional, reutilizar structs ou manter SQL mais legível. Se seu SQL de construção de JSON ficar difícil de ler, também fica difícil de otimizar.
Uma boa regra: deixe o Postgres filtrar, ordenar e agregar. Deixe o Go cuidar da apresentação final.
Se você gera APIs rapidamente (por exemplo com Koder.ai), adicionar flags de include cedo ajuda a evitar endpoints que incham com o tempo. Também dá uma forma segura de adicionar campos sem tornar cada resposta mais pesada.
Um passo a passo de ajuste antes dos primeiros usuários
Você não precisa de um laboratório enorme para achar a maioria dos problemas de desempenho. Um passe curto e repetível traz à tona os problemas que viram outages quando o tráfego aparece, especialmente quando o ponto de partida é código gerado que você planeja lançar.
Antes de mudar qualquer coisa, escreva uma linha de base pequena:
- p95 e p99 de latência dos endpoints mais movimentados
- taxa de erros e timeouts
- CPU do banco e conexões ativas
- as 5 queries mais lentas por tempo total (não só a pior)
O passe de tuning
Comece pequeno, mude uma coisa por vez e re-teste após cada mudança.
-
Rode um load test de 10 a 15 minutos que pareça uso real. Bata nos endpoints que seus primeiros usuários vão usar (login, páginas de listagem, busca, create). Depois ordene rotas por latência p95 e tempo total gasto.
-
Verifique pressão de conexão antes de tunar SQL. Um pool grande demais sobrecarrega o Postgres. Um pool pequeno demais cria esperas longas. Procure aumento no tempo de espera para adquirir conexão e contagens de conexão que disparam durante rajadas. Ajuste limites de pool e idle primeiro, então reexecute o mesmo teste.
-
EXPLAINas queries mais lentas e corrija o maior sinal de alerta. Os culpados habituais são scans de tabela cheia em tabelas grandes, sorts em conjuntos grandes e joins que explodem contagens de linhas. Pegue a query pior e torne-a entediante. -
Adicione ou ajuste um índice e re-teste. Índices ajudam quando batem com seu
WHEREeORDER BY. Não adicione cinco de uma vez. Se o endpoint lento é “listar pedidos por user_id ordenado por created_at”, um índice composto em(user_id, created_at)pode ser a diferença entre instantâneo e doloroso. -
Enxugue respostas e paginação, então re-teste de novo. Se um endpoint retorna 50 linhas com blobs JSON grandes, banco, rede e cliente pagam o preço. Retorne apenas os campos que a UI precisa e prefira paginação que não degrade conforme a tabela cresce.
Mantenha um changelog simples: o que mudou, por que e o que moveu no p95. Se uma mudança não melhorou sua linha de base, reverta e siga adiante.
Erros comuns e armadilhas para evitar
A maior parte dos problemas de desempenho em APIs Go com Postgres é autoinfligida. A boa notícia é que algumas checagens pegam a maioria antes de o tráfego real chegar.
Uma armadilha clássica é tratar o tamanho do pool como um botão de velocidade. Configurá-lo “o mais alto possível” frequentemente deixa tudo mais lento. O Postgres passa mais tempo gerenciando sessões, memória e locks, e seu app começa a timing out em ondas. Um pool menor e estável com concorrência previsível geralmente vence.
Outro erro comum é “indexar tudo”. Índices extras ajudam leituras, mas também tornam writes mais lentos e podem alterar planos de consulta de maneiras surpreendentes. Se sua API insere ou atualiza frequentemente, cada índice extra adiciona trabalho. Meça antes e depois, e re-verifique planos após adicionar um índice.
Dívida de paginação entra silenciosamente. Paginação por offset parece ok no início, depois p95 sobe porque o banco tem que pular mais e mais linhas.
Tamanho do payload JSON é outro imposto escondido. Compressão ajuda banda, mas não elimina o custo de construir, alocar e parsear objetos grandes. Corte campos, evite aninhamento profundo e retorne só o que a tela precisa.
Se você só observa tempo médio de resposta, vai perder onde a dor do usuário começa. p95 (e às vezes p99) é onde saturação de pool, espera por locks e planos lentos aparecem primeiro.
Um auto-check rápido pré-lançamento:
- Observe tempo de espera do pool e contagem de conexões do Postgres durante um pequeno load test.
- Compare média vs p95 para o mesmo endpoint.
- Verifique que paginação não degrada quando a tabela está 10x maior.
- Inspecione tamanhos de resposta para endpoints de listagem (bytes importam).
- Reexecute
EXPLAINapós adicionar índices ou mudar filtros.
Checklist rápido e próximos passos antes do lançamento
Antes de chegarem usuários reais, você quer evidência de que sua API se mantém previsível sob estresse. O objetivo não é número perfeito. É pegar os poucos problemas que causam timeouts, picos ou um banco que para de aceitar trabalho.
Rode checagens em um staging que se pareça com produção (tamanho de DB similar, mesmos índices, mesmas configurações de pool): meça p95 por endpoint chave sob carga, capture suas queries lentas por tempo total, observe tempo de espera no pool, rode EXPLAIN (ANALYZE, BUFFERS) na pior query para confirmar que está usando o índice esperado e cheque tamanhos de payload nas rotas mais ativas.
Faça então um teste do pior caso que imite como produtos quebram: solicite uma página profunda, aplique o filtro mais amplo e tente com cold start (reinicie a API e acerte a mesma requisição primeiro). Se paginação profunda fica mais lenta a cada página, mude para paginação por cursor antes do lançamento.
Anote seus padrões por padrão para que o time faça escolhas consistentes depois: limites e timeouts de pool, regras de paginação (tamanho máximo de página, se offset é permitido, formato do cursor), regras de query (selecionar só colunas necessárias, evitar SELECT *, limitar filtros caros) e regras de logging (limiar de query lenta, quanto tempo manter amostras, como rotular endpoints).
Se você constrói e exporta serviços Go + Postgres com Koder.ai, fazer um curto planejamento antes do deploy ajuda a manter filtros, paginação e formas de resposta intencionais. Depois que você começa a ajustar índices e formas de query, snapshots e rollback facilitam desfazer uma “correção” que ajuda um endpoint mas prejudica outros. Se quiser um lugar único para iterar nesse workflow, Koder.ai on koder.ai é projetado para gerar e refinar esses serviços via chat, e então exportar o código-fonte quando estiver pronto.
Perguntas frequentes
Como eu descubro rapidamente se minha API Go está lenta por causa do Postgres ou por causa do meu código?
Comece separando o tempo de espera do DB do tempo de trabalho do app.
- Se o banco de dados está lento, o handler passa a maior parte do tempo esperando a query. O uso de CPU do Go costuma permanecer normal enquanto várias requisições ficam “em voo”.
- Se o app está lento, as queries retornam rápido, mas o tempo é gasto construindo objetos, fazendo queries extras por linha, serializando JSON grande ou fazendo logging. CPU e memória do Go normalmente aumentam com o tamanho da resposta.
Adicione medições simples em “espera por conexão” e “execução da query” para ver qual lado domina.
Quais métricas devo acompanhar primeiro antes de qualquer ajuste?
Use uma linha de base pequena e repetível:
- p95 de latência por endpoint chave (não média)
- taxa de erros (5xx, timeouts, cancelamentos)
- tempo de DB por requisição (tempo esperando o Postgres)
Escolha um alvo claro como “p95 abaixo de 200 ms com 50 usuários concorrentes, erros abaixo de 0,5%”. Depois, mude apenas uma coisa por vez e reexecute a mesma mistura de requisições.
Devo ativar o log de queries lentas do Postgres, e qual limiar é prático?
Ative o log de queries lentas com um limiar baixo nos testes pré-lançamento (por exemplo, 100–200 ms) e registre a declaração completa para que você possa copiar para um cliente SQL.
Mantenha isso temporário:
- Fica muito barulhento rápido em produção.
- Pode adicionar overhead se você logar demais.
Depois de encontrar os piores culpados, mude para amostragem ou aumente o limiar.
Quais são boas configurações iniciais de pool de conexão para uma API Go com Postgres?
Um padrão prático é um pequeno múltiplo dos núcleos de CPU por instância da API, muitas vezes 5–20 conexões abertas no máximo, com número similar de conexões ociosas e reciclagem de conexões a cada 30–60 minutos.
Dois modos de falha comuns:
- Pool pequeno demais: requisições esperam por conexão embora o tempo de query pareça ok.
- Pool grande demais: Postgres fica sobrecarregado com muitas sessões ativas e a latência fica desigual.
Lembre-se que pools se multiplicam entre instâncias (20 conexões × 10 instâncias = 200 conexões).
Como confirmar que o pool de conexões é o gargalo (e não o SQL)?
Meça as chamadas ao DB em duas partes:
- Tempo esperando por conexão (espera no pool)
- Tempo executando a query (trabalho do Postgres)
Se a maior parte do tempo é espera no pool, ajuste o tamanho do pool, timeouts e número de instâncias. Se a maior parte é execução da query, foque em EXPLAIN e índices.
Confirme também que você sempre fecha rows prontamente para devolver conexões ao pool.
O que devo checar primeiro no EXPLAIN quando um endpoint está lento?
Execute EXPLAIN (ANALYZE, BUFFERS) no SQL exato que sua API envia e procure:
- Seq Scan em uma tabela grande
- Grande diferença entre linhas estimadas vs linhas reais
- Sort dominando o tempo (freqüentemente com
ORDER BY) - “Rows Removed by Filter” muito alto
- Muitos blocos de leitura compartilhada em BUFFERS (leitura pesada)
Corrija o maior sinal de alerta primeiro; não tente ajustar tudo de uma vez.
Como escolher o índice certo para um endpoint de listagem com filtros e ordenação?
Índices devem corresponder ao que o endpoint realmente faz: filtros + ordem de ordenação.
Abordagem prática:
- Crie um índice composto para o padrão comum
WHERE+ORDER BY. - Mantenha-o pequeno (2 colunas é comum; 3 às vezes).
- Coloque o filtro mais seletivo primeiro, depois a coluna de ordenação.
Exemplo: se você filtra por user_id e ordena pelos mais novos, um índice como (user_id, created_at DESC) costuma resolver p95 instável.
Quando um índice parcial vale a pena no Postgres?
Use um índice parcial quando a maior parte do tráfego atinge um subconjunto previsível de linhas.
Padrão de exemplo:
- Muitas leituras apenas para
active = true - Poucas queries para linhas inativas
Um índice parcial como ... WHERE active = true fica menor, tem mais chance de caber em memória e reduz overhead de escrita em comparação com indexar tudo.
Confirme com EXPLAIN que o Postgres realmente o usa para suas queries de alto tráfego.
Por que a paginação com LIMIT/OFFSET fica mais lenta com o tempo, e o que devo usar em vez disso?
LIMIT/OFFSET fica mais lento em páginas profundas porque o Postgres ainda precisa pular (e muitas vezes ordenar) as linhas que você está ignorando. A página 1 pode tocar poucas dezenas de linhas; a página 500 pode forçar o banco a escanear e descartar dezenas de milhares só para devolver 20 resultados.
Prefira a paginação por chave (keyset/cursor):
- Use uma ordenação estável mais um desempate único (geralmente
id). - Mantenha o
ORDER BYidêntico entre requisições. - Codifique
(created_at, id)ou similar em um cursor.
Isso mantém o custo de cada página aproximadamente constante conforme a tabela cresce.
Minhas queries do BD são rápidas, mas as respostas continuam lentas — devo reduzir os payloads JSON?
Quase sempre sim para endpoints de listagem. A resposta mais rápida é a que você não envia.
Ganhos práticos:
- Selecione apenas colunas necessárias (evite
SELECT *). - Adicione
include=oufields=para que clientes optem por campos pesados. - Limite arrays aninhados (por exemplo, apenas os 10 eventos mais recentes) e busque o resto via endpoint dedicado.
- Evite padrões N+1 (50 linhas + 50 queries extras). Use joins ou queries em lote.
Você frequentemente reduz CPU do Go, pressão de memória e latência de cauda apenas reduzindo o payload.