Solução de Problemas de Consultas Lentas no MySQL: Um Guia Passo a Passo

Um fluxo de trabalho prático para encontrar consultas lentas no MySQL, ler planos de execução, corrigir índices e comprovar que a mudança funcionou.

Solução de Problemas de Consultas Lentas no MySQL: Um Guia Passo a Passo

A solução de problemas de consultas lentas no MySQL começa com uma regra desconfortável: não adivinhe apenas pelo texto da consulta. Uma consulta que parece feia pode ser inofensiva porque é executada uma vez por dia. Uma consulta que parece simples pode estar arruinando o banco de dados porque é executada milhares de vezes por minuto, varre muitas linhas ou espera atrás de bloqueios.

O fluxo de trabalho útil é chato da melhor maneira possível. Capture consultas lentas reais, agrupe-as por custo, inspecione o plano de execução, mude uma coisa e meça novamente. Isso evita que você adicione índices aleatórios, altere configurações globais cegamente ou culpe o MySQL quando a aplicação está enviando um padrão de consulta evitável.

Geralmente começo com três perguntas:

  • Qual consulta está prejudicando os usuários, não apenas parecendo suspeita?
  • O tempo é gasto lendo linhas, ordenando, esperando em bloqueios ou esperando na aplicação?
  • Posso provar a correção com EXPLAIN, medição de tempo e novos dados do log lento?

Comece com o log de consultas lentas

O log de consultas lentas do MySQL registra declarações que ultrapassam o limite configurado. De acordo com o manual do MySQL, o log está desabilitado por padrão, long_query_time tem como padrão 10 segundos, e uma declaração normalmente precisa ser executada por pelo menos esse tempo e examinar pelo menos min_examined_row_limit linhas antes de ser registrada. Se log_queries_not_using_indexes estiver habilitado, o MySQL também pode registrar declarações que não usam índices para buscas de linhas. Essa opção é útil durante o diagnóstico, mas pode produzir muito ruído em sistemas ocupados.

Uma configuração inicial prática se parece com isso:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
log_output = FILE

Em muitos sistemas de produção, 1 segundo é uma primeira passagem razoável. Para uma API sensível à latência, você pode temporariamente reduzi-lo para 0,5 ou 0,2. Faça isso com um plano e um monitoramento de espaço em disco. Um banco de dados de alto tráfego pode escrever uma quantidade surpreendente de dados de log lento assim que o limite cai.

Você pode verificar as configurações ativas a partir de uma sessão MySQL:

SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'slow_query_log_file';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_output';
SHOW VARIABLES LIKE 'min_examined_row_limit';
SHOW VARIABLES LIKE 'log_queries_not_using_indexes';

Para uma investigação temporária, você pode habilitar o log sem editar o arquivo de configuração:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

Lembre-se que as alterações SET GLOBAL podem não sobreviver a uma reinicialização a menos que você também as persista através do seu processo normal de configuração. No MySQL 8, algumas equipes usam SET PERSIST, mas ainda prefiro comprometer a configuração desejada no gerenciamento de configuração para que o próximo operador possa vê-la.

Se você ativar log_queries_not_using_indexes, considere também definir log_throttle_queries_not_using_indexes para que um endpoint barulhento não inunde o log. O MySQL suporta essa limitação especificamente porque o log sem índice pode crescer rapidamente.

Agrupe o log antes de ler consultas individuais

Logs lentos brutos são repetitivos. Você pode ver a mesma consulta centenas de vezes com IDs diferentes. Ler o arquivo de cima a baixo perde tempo e faz consultas raras e assustadoras parecerem mais importantes do que as comuns e caras.

Comece com mysqldumpslow, que acompanha as instalações do MySQL em muitos ambientes:

sudo mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log

Isso pede os dez principais padrões classificados por tempo de consulta. As flags exatas variam por versão e plataforma, então verifique mysqldumpslow --help se o seu comando se comportar de forma diferente. Classificações úteis incluem tempo total, tempo médio, tempo de bloqueio e linhas examinadas.

Para investigações em produção, pt-query-digest do Percona Toolkit é frequentemente melhor porque fornece agrupamento mais rico e detalhes no estilo de percentil. A ferramenta não é mágica; ela simplesmente evita que você faça aritmética manualmente. O importante é classificar por impacto. Uma consulta que leva oito segundos uma vez por noite pode ser menos urgente do que uma consulta que leva 120 milissegundos, mas é executada 600 vezes por segundo.

Ao ler a saída agrupada, procure por padrões:

  • Alto tempo total: provavelmente visível ao usuário ou pesado em recursos.
  • Alta contagem: geralmente um loop de aplicação ou cache ausente.
  • Alto número de linhas examinadas com baixo número de linhas enviadas: geralmente um problema de indexação ou filtragem.
  • Alto tempo de bloqueio: possivelmente uma transação, contenção de escrita, bloqueio de metadados ou problema de DDL.

Não assuma que Rows_examined alto é sempre ruim. Consultas de relatórios e trabalhos em lote às vezes varrem intencionalmente. A questão é se a varredura corresponde ao trabalho e se acontece no momento certo.

Reproduza uma consulta com segurança

Escolha um padrão de consulta e obtenha uma amostra real com parâmetros. Se o log lento normalizou literais, encontre a consulta original nos logs da aplicação, traces de APM ou na entrada bruta do log lento.

Antes de executá-la manualmente, verifique o raio de explosão. Um SELECT lento em uma réplica geralmente é seguro. Um UPDATE lento em produção não é algo para ser reexecutado casualmente. Para consultas de escrita, inspecione primeiro o plano e o padrão de transação, ou teste contra uma cópia de staging com dados realistas.

Uma nota de rascunho útil para cada consulta se parece com isso:

Endpoint: GET /customers/123/orders
Padrão de consulta: pedidos por cliente e status, mais recentes primeiro
Observado: 1,8s média, 420k linhas examinadas, 20 linhas enviadas
Tamanho da tabela: 12M linhas
Tamanho esperado do resultado: uma página de pedidos
Suspeita: índice composto ausente para customer_id, status, created_at

Essa nota mantém o trabalho vinculado a um caminho de usuário real, em vez de um trecho SQL aleatório.

Use EXPLAIN, depois leia como um operador

Execute EXPLAIN na consulta lenta:

EXPLAIN
SELECT id, customer_id, status, created_at, total
FROM orders
WHERE customer_id = 123
  AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

Para MySQL 8, EXPLAIN ANALYZE pode executar a consulta e mostrar informações reais de tempo. Use-o com cuidado em consultas caras porque ele realmente executa a declaração. Para consultas SELECT simples em um ambiente controlado, pode ser muito útil.

As colunas que verifico primeiro são type, possible_keys, key, rows, filtered e Extra.

type informa o padrão de acesso. const, eq_ref, ref e range geralmente são bons sinais. index significa que o MySQL está varrendo um índice, o que ainda pode ser muito trabalho. ALL significa uma varredura completa da tabela. Uma varredura completa não está automaticamente errada em uma tabela pequena, mas é suspeita em uma tabela quente com milhões de linhas.

key mostra o índice que o MySQL escolheu. Se possible_keys listar um índice promissor, mas key for diferente, o otimizador pode pensar que o outro índice é mais barato. Isso pode acontecer devido à baixa seletividade, estatísticas desatualizadas ou um índice que não corresponde ao filtro e à ordenação juntos.

rows é uma estimativa, não uma promessa. Se a estimativa estiver extremamente errada, execute ANALYZE TABLE durante uma janela de manutenção apropriada ou revise se a distribuição dos dados é distorcida.

Extra frequentemente conta a história. Using filesort significa que o MySQL precisa de uma etapa de ordenação separada; não significa necessariamente ordenação em disco, mas vale a pena verificar quando o conjunto de resultados é grande. Using temporary aparece frequentemente com agrupamento, consultas distintas ou ordenações complexas. Using index pode ser bom porque a consulta é satisfeita a partir do índice sem ler linhas da tabela.

Corrija índices com a forma completa da consulta em mente

A correção mais comum para consultas lentas não é "adicionar um índice à coluna na cláusula WHERE". A melhor regra é: construa um índice que corresponda à forma como a consulta filtra, junta, ordena e limita linhas.

Para a consulta de pedidos acima, um índice de coluna única em customer_id pode ajudar, mas ainda pode deixar o MySQL ordenando muitas linhas para aquele cliente. Um índice composto é frequentemente mais útil:

CREATE INDEX idx_orders_customer_status_created
ON orders (customer_id, status, created_at);

Se a consulta ordenar pelos mais recentes primeiro, o MySQL pode frequentemente varrer o índice em ordem reversa. No MySQL 8, você também pode definir índices descendentes quando isso se encaixa em um padrão maior:

CREATE INDEX idx_orders_customer_status_created_desc
ON orders (customer_id, status, created_at DESC);

A ordem das colunas importa. Coloque filtros de igualdade primeiro, depois colunas de intervalo ou ordenação quando isso corresponder à consulta. Por exemplo, com WHERE customer_id = ? AND status = ? ORDER BY created_at DESC LIMIT 20, customer_id, status, created_at é geralmente mais útil do que created_at, customer_id, status.

Não adicione todo índice que pareça útil. Índices aceleram leituras, mas retardam escritas e consomem armazenamento. Se a tabela receber muitas inserções ou atualizações, um novo índice composto tem um custo real. Verifique os índices existentes primeiro:

SHOW INDEX FROM orders;

Às vezes, a resposta certa é substituir dois índices fracos por um índice composto melhor, não manter todos os três.

Reescreva consultas que bloqueiam o uso de índices

Algumas consultas lentas são lentas porque escondem valores indexados atrás de funções ou padrões que o MySQL não pode usar eficientemente.

Esta versão é comum e dolorosa:

SELECT *
FROM orders
WHERE YEAR(created_at) = 2026;

Se created_at estiver indexado, envolvê-lo em YEAR() pode impedir uma busca de intervalo normal. Escreva o predicado como um intervalo:

SELECT id, customer_id, status, created_at, total
FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at <  '2027-01-01';

A mesma ideia se aplica a pesquisas com curinga no início:

WHERE email LIKE '%@example.com'

Um índice B-tree normal não pode pular para o meio de uma string. Se pesquisas de sufixo forem importantes, você pode precisar de uma coluna gerada, um campo normalizado separado ou um sistema de busca projetado para esse caso de uso.

Também fique atento a SELECT *. Parece inofensivo durante o desenvolvimento, mas pode forçar o MySQL a ler linhas da tabela quando uma projeção menor poderia usar um índice de cobertura. Também envia dados desnecessários pela rede.

Verifique bloqueios quando o plano de consulta parecer bom

Uma consulta pode ter um plano decente e ainda ser lenta porque está esperando. O Lock_time do log lento pode apontar nessa direção, mas não explica todos os tipos de espera. Se os usuários relatarem pausas aleatórias, verifique as sessões ativas:

SHOW PROCESSLIST;

No MySQL 8, o Performance Schema e as visões do schema sys podem fornecer melhores detalhes, dependendo de como o servidor está configurado. Para uma olhada rápida, geralmente verifico transações de longa duração e declarações bloqueadas antes de alterar índices.

Um exemplo real: uma consulta UPDATE orders SET status = ? WHERE id = ? deve ser rápida. Se aparecer no log lento com uma busca por chave primária, o problema pode ser uma transação que deixou a linha bloqueada enquanto fazia trabalho não relacionado. A correção não é outro índice. A correção é encurtar a transação e mover chamadas externas lentas para fora dela.

Bloqueios de metadados podem criar uma armadilha semelhante. Uma migração executando ALTER TABLE pode esperar por uma transação antiga, enquanto novas consultas se acumulam atrás do DDL pendente. O log de consultas lentas mostrará sintomas, mas a causa raiz é o comportamento da implantação.

Ajuste as configurações do servidor somente após o trabalho com consultas

A configuração importa, mas é fácil usar demais como primeira resposta. Se uma consulta varre cinco milhões de linhas para retornar dez, aumentar a memória pode apenas fazer o plano ruim doer menos.

Para sistemas pesados em InnoDB, innodb_buffer_pool_size é a primeira configuração a revisar. Em um servidor MySQL dedicado, geralmente é definido para uma grande parte da memória, mas o valor certo depende do que mais é executado no host, tamanho do conjunto de dados e carga de trabalho. Não copie uma porcentagem cegamente de um post de blog.

Também verifique se o banco de dados está esperando em disco. Se o conjunto de trabalho não couber na memória, ou o armazenamento estiver saturado, até mesmo consultas bem indexadas podem parar. Combine a revisão de consultas com métricas do host: CPU, latência de disco, IOPS, pressão de memória e número de conexões.

Pools de conexão podem fazer consultas lentas parecerem piores. Se um endpoint disparar muitas declarações lentas, o pool enche, solicitações não relacionadas esperam por conexões e todo o aplicativo parece quebrado. Nesse caso, corrigir a consulta ainda é o trabalho principal, mas os limites do pool e os timeouts determinam quão graciosamente o sistema falha.

Prove a correção

Após adicionar um índice ou reescrever uma consulta, execute EXPLAIN novamente. Você quer ver menos linhas estimadas, uma chave melhor escolhida e menos etapas extras caras. Em seguida, teste a consulta real com parâmetros realistas.

Não pare em uma execução rápida. O cache quente pode esconder problemas. Tente casos comuns, grandes e estranhos:

  • Um cliente com muitos pedidos.
  • Um cliente sem pedidos correspondentes.
  • Um intervalo de datas que abrange um período movimentado.
  • Um valor de status que corresponde à maioria das linhas.

Depois, observe o log lento após a implantação. O melhor resultado não é "a consulta parecia melhor no staging". O melhor resultado é que o padrão de consulta desaparece dos principais infratores, a pressão da CPU ou I/O diminui e o caminho do usuário fica mais rápido.

A solução de problemas de consultas lentas no MySQL é principalmente coleta de evidências disciplinada. Habilite o log com limites sensatos, agrupe os padrões caros, inspecione o plano, corrija a forma da consulta e valide com dados novos. Esse hábito evita tanto a correção insuficiente quanto a correção excessiva, que é exatamente o que você quer quando o banco de dados já está sob pressão.