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.
"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.
