Pular para o conteúdo
banco-de-dados

Otimização de Banco de Dados MySQL: Do Lento ao Rápido

Técnicas práticas para otimizar performance de MySQL em produção: índices, EXPLAIN, query cache, configurações de InnoDB e monitoramento contínuo.

Douglas M. Pereira4 min de leitura
mysqlperformanceíndicesotimizaçãobanco de dados

"O banco está lento" — as três palavras mais temidas em produção

Toda equipe já viveu esse momento. A aplicação que rodava bem começa a ficar lenta. Os usuários reclamam. O CEO pergunta o que está acontecendo. E a resposta honesta é: provavelmente uma query ruim, falta de índice ou configuração inadequada do MySQL.

Este guia cobre as causas mais comuns e como resolvê-las.

Passo 1: Identificar as queries lentas

Ative o slow query log:

# my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1       # queries acima de 1 segundo
log_queries_not_using_indexes = 1

Analise com pt-query-digest (Percona Toolkit):

pt-query-digest /var/log/mysql/slow.log | head -100

O relatório mostra as queries por tempo total gasto, frequência e tempo médio — exatamente o que você precisa para priorizar.

Passo 2: EXPLAIN e EXPLAIN ANALYZE

Para cada query lenta, rode o EXPLAIN:

EXPLAIN SELECT 
  o.id,
  o.created_at,
  c.name as customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending'
  AND o.created_at > '2025-01-01'
ORDER BY o.created_at DESC;

O que procurar no resultado:

| Coluna | Problema | |--------|---------| | type = ALL | Varredura completa da tabela — muito ruim | | rows = 500000 | Processando metade da tabela | | Extra = Using filesort | Ordenação em memória/disco | | Extra = Using temporary | Tabela temporária — sinal de alerta |

Passo 3: Índices que realmente importam

Índice composto: a ordem importa

-- Ruim: dois índices separados
CREATE INDEX idx_status ON orders(status);
CREATE INDEX idx_created ON orders(created_at);

-- Bom: índice composto na ordem certa (WHERE primeiro, ORDER BY depois)
CREATE INDEX idx_status_created ON orders(status, created_at);

A regra: colunas de igualdade (=) antes de colunas de range (>, <, BETWEEN).

Índice cobridor (covering index)

Um índice que contém todas as colunas da query evita acesso à tabela:

-- Query vai à tabela buscar o customer_id
CREATE INDEX idx_status ON orders(status);

-- Query resolve tudo pelo índice
CREATE INDEX idx_status_covering ON orders(status, created_at, customer_id, id);

Quando NÃO criar índice

  • Colunas de baixa cardinalidade (status com 3 valores) em tabelas pequenas
  • Tabelas que sofrem muito mais writes que reads (índice tem custo de manutenção)
  • Mais de 5–6 índices por tabela em geral prejudica performance de write

Configurações do InnoDB que muita gente ignora

# Tamanho do buffer pool: deve ser 70-80% da RAM disponível para MySQL
innodb_buffer_pool_size = 8G

# Múltiplas instâncias do buffer pool (uma por CPU core, até 8)
innodb_buffer_pool_instances = 8

# Flush mais eficiente (SSD: O_DIRECT)
innodb_flush_method = O_DIRECT

# Log de transação maior reduz checkpoint frequency
innodb_log_file_size = 512M

# Paralelismo em I/O
innodb_read_io_threads = 8
innodb_write_io_threads = 8

Análise de queries problemáticas comuns

O N+1 que mata a performance

# Código ruim: 1 query para listar + N queries para detalhe
orders = db.query("SELECT * FROM orders LIMIT 100")
for order in orders:
    customer = db.query("SELECT * FROM customers WHERE id = ?", order.customer_id)
    # 100 queries extras!

# Correto: JOIN ou IN
orders = db.query("""
    SELECT o.*, c.name, c.email 
    FROM orders o 
    JOIN customers c ON c.id = o.customer_id
    LIMIT 100
""")

Paginação com OFFSET em tabelas grandes

-- Lento: MySQL precisa ler e descartar os primeiros 100.000 registros
SELECT * FROM orders ORDER BY id DESC LIMIT 20 OFFSET 100000;

-- Rápido: keyset pagination com cursor
SELECT * FROM orders 
WHERE id < :last_seen_id 
ORDER BY id DESC 
LIMIT 20;

COUNT(*) em tabelas grandes

-- Lento em tabelas grandes com WHERE complexo
SELECT COUNT(*) FROM logs WHERE user_id = 123;

-- Alternativa: manter contador desnormalizado
UPDATE user_stats SET log_count = log_count + 1 WHERE user_id = 123;
SELECT log_count FROM user_stats WHERE user_id = 123;

Monitoramento contínuo

Ferramentas essenciais:

  • Percona Monitoring and Management (PMM): gratuito, melhor dashboard para MySQL
  • pt-query-digest: análise de slow log
  • MySQLTuner: script que analisa variáveis e sugere ajustes
# PMM via Docker (servidor de monitoramento)
docker run -d -p 80:80 -p 443:443 \
  --name pmm-server \
  percona/pmm-server:latest

Conclusão

Otimização de MySQL bem-feita segue uma sequência: identificar as queries lentas com slow log, entender o plano de execução com EXPLAIN, criar índices adequados e ajustar configurações do InnoDB. O resultado é uma aplicação que escala com os dados sem precisar de hardware cada vez maior.