
Você migrou seu sistema para Go. As goroutines são leves, o binário é rápido, a latência caiu drasticamente. Os primeiros meses são uma festa. Mas, de repente, a performance começa a piorar. O tempo de resposta sobe. A CPU do banco de dados dispara. E o pior: seu código Go continua tão eficiente quanto antes.
O problema não está no seu código. Está no banco de dados.
Go é uma das linguagens mais rápidas para backend, mas nenhuma velocidade de linguagem compensa queries SQL ineficientes. Uma única consulta mal escrita pode transformar uma API que rodava em 10ms em uma que leva 10 segundos – e não há goroutine que resolva.
Neste post, vamos mostrar os principais anti-padrões de SQL em sistemas Go, como identificá-los e corrigi-los, e as práticas que a Jacobus Software adota para garantir que o banco nunca seja o gargalo.
O mito do “Go é rápido, o banco que segura”
Muitos times acreditam que, como Go é performático, podem escrever queries “simples” e confiar no banco. Isso é perigoso. O gargalo mais comum em sistemas Go modernos não é a linguagem – é o acesso a dados.
Os vilões mais frequentes:
- Problema N+1 – uma query dentro de um loop
- Falta de índices – scans de tabela inteira
- Seleção de colunas desnecessárias –
SELECT *matando a largura de banda - Transações longas – locks segurando recursos
- Conversões implícitas – índice ignorado porque comparou string com inteiro
Vamos a cada um.
1. O problema N+1: o assassino silencioso
É o erro mais comum em Go com ORMs (GORM, ent) ou mesmo com SQL puro. Ele acontece quando você carrega uma lista de entidades e, para cada uma, faz uma nova query.
Exemplo ruim (N+1)
// 1 query para buscar todos os pedidos
rows, _ := db.Query("SELECT id, cliente_id FROM pedidos LIMIT 100")
for rows.Next() {
var pedido Pedido
rows.Scan(&pedido.ID, &pedido.ClienteID)
// + 1 query por pedido para buscar o cliente (N queries)
db.QueryRow("SELECT nome, email FROM clientes WHERE id = ?", pedido.ClienteID).Scan(&cliente.Nome, &cliente.Email)
}
Com 100 pedidos, são 101 queries. Se cada uma leva 5ms, já são 500ms – fora latência de rede.
Solução correta (JOIN ou IN)
query := `
SELECT p.id, p.cliente_id, c.nome, c.email
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
LIMIT 100
`
rows, _ := db.Query(query)
// Agora todos os dados vêm em uma única query.
Go + GORM: use Preload apenas quando necessário, ou Joins. Evite o modo “lazy loading” padrão.
2. Índices: a diferença entre milissegundos e segundos
Um índice bem projetado acelera consultas em ordens de magnitude. Um índice ausente força o banco a varrer a tabela inteira (full scan).
Exemplo sem índice (lento)
SELECT * FROM pedidos WHERE status = 'pendente' AND data_criacao > '2026-01-01';
Se status não tem índice, o banco varre milhões de linhas.
Solução: crie índices seletivos
CREATE INDEX idx_pedidos_status_data ON pedidos(status, data_criacao);
Em Go, com migrations: use ferramentas como golang-migrate para versionar índices junto com o código. Nunca adicione índices manualmente em produção.
Quando muitos índices atrapalham
Índices aceleram leituras mas desaceleram escritas (INSERT/UPDATE/DELETE). Em Go, se seu serviço tem alta taxa de escrita (ex: logs, eventos), evite índices excessivos. Prefira bancos especializados (ClickHouse, TimescaleDB) para analytics.
3. SELECT * é quase sempre um erro
SELECT * devolve todas as colunas, incluindo campos BLOB, TEXT ou grandes JSONs que você nem usa. Isso:
- Aumenta o tráfego de rede entre o banco e Go
- Força o banco a ler mais páginas do disco
- Consome mais memória no Go (cada linha maior)
Exemplo ruim
rows, _ := db.Query("SELECT * FROM produtos")
Melhor prática
rows, _ := db.Query("SELECT id, nome, preco FROM produtos WHERE ativo = true")
No Go com sqlx ou GORM: use structs com tags db:"coluna" e selecione apenas as colunas necessárias.
4. Transações longas e locks
Go facilita concorrência, mas se você mantém uma transação aberta enquanto processa dados ou chama APIs externas, pode travar o banco inteiro.
Exemplo perigoso
tx, _ := db.Begin()
var pedido Pedido
tx.Get(&pedido, "SELECT * FROM pedidos WHERE id = ? FOR UPDATE", id)
// Chama API de pagamento externa (pode levar 2 segundos)
resp, _ := http.Get("https://gateway.pagamento.com/processar/" + pedido.ID)
tx.Exec("UPDATE pedidos SET status = 'pago' WHERE id = ?", id)
tx.Commit()
A linha FOR UPDATE mantém um lock na linha do pedido por todo o tempo da chamada HTTP. Se muitas requisições concorrentes fizerem isso, o banco ficará lento ou terá deadlocks.
Correção
- Execute operações externas antes de iniciar a transação
- Ou use transações menores e consistência eventual (eventos)
// Primeiro, faz a operação externa
resp, _ := http.Get(...)
// Depois, transação curta
tx, _ := db.Begin()
tx.Exec("UPDATE pedidos SET status = 'pago' WHERE id = ?", id)
tx.Commit()
5. Conversões implícitas que matam índices
Se você compara uma coluna varchar com um número, o banco converte a coluna implicitamente – e frequentemente ignora o índice.
Exemplo ruim
-- telefone é VARCHAR, mas passamos número
SELECT * FROM clientes WHERE telefone = 11999999999;
O banco faz CAST(telefone AS int) para cada linha, ignorando qualquer índice em telefone.
Correção: use o tipo correto
SELECT * FROM clientes WHERE telefone = '11999999999';
Em Go, com database/sql: sempre passe os parâmetros com o tipo compatível com a coluna. Evite converter no SQL.
Como a Jacobus monitora performance de banco em Go
Em nossos projetos, adotamos:
- Explicação de queries em CI – rodamos
EXPLAINdas queries críticas em cada PR (comgo testexecutando em banco de teste). - Log de slow queries configurado no PostgreSQL/MySQL – qualquer query acima de 100ms vai para o sistema de logs com
trace_idcompatível. - Métricas Prometheus – Exportamos número de queries por segundo, erros de banco, e latência por tipo de operação.
- Uso de connection pool bem dimensionado – Go padrão
sql.DBpermite configurarMaxOpenConns,MaxIdleConns. Muitos erros de “too many connections” vêm de valores default inadequados.
Exemplo de configuração recomendada
db, _ := sql.Open("postgres", connString)
db.SetMaxOpenConns(25) // depende do limite do banco
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
Go não é mágica: o banco ainda é rei
Go resolve problemas de concorrência e CPU, mas dados são o coração do sistema. Nenhuma quantidade de goroutines vai consertar uma query que varre 10 milhões de linhas desnecessariamente.
Na Jacobus Software, tratamos o banco como cidadão de primeira classe. Isso significa:
- Revisão de queries em todos os PRs
- Índices projetados para padrões reais de acesso
- Uso cauteloso de ORMs – frequentemente preferimos SQL puro com
sqlxpara queries complexas - Monitoramento proativo com Prometheus + Grafana
O resultado: sistemas Go que não só são rápidos no código, mas rápidos do teclado do usuário até o disco.
🗄️ Seu sistema Go é rápido, mas o banco está te freando?
Nossos especialistas auditam queries, índices e configurações de banco para eliminar gargalos de dados – e garantir que todo o potencial do Go seja aproveitado.
