Custo oculto da ineficiência de dados: Como queries SQL mal escritas podem destruir sua performance em Go

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:

  1. Problema N+1 – uma query dentro de um loop
  2. Falta de índices – scans de tabela inteira
  3. Seleção de colunas desnecessáriasSELECT * matando a largura de banda
  4. Transações longas – locks segurando recursos
  5. 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:

  1. Explicação de queries em CI – rodamos EXPLAIN das queries críticas em cada PR (com go test executando em banco de teste).
  2. Log de slow queries configurado no PostgreSQL/MySQL – qualquer query acima de 100ms vai para o sistema de logs com trace_id compatível.
  3. Métricas Prometheus – Exportamos número de queries por segundo, erros de banco, e latência por tipo de operação.
  4. Uso de connection pool bem dimensionado – Go padrão sql.DB permite configurar MaxOpenConns, 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 sqlx para 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.

👉 Fale com a Jacobus Software

Rolar para cima