MariaDB lento? Como achar e corrigir as consultas que estão derrubando seu sistema

Antes de pedir um servidor maior, descubra qual consulta está pesando. O MariaDB já traz tudo o que você precisa para isso.
Resumo para quem tem pressa: na grande maioria dos casos de "o banco está lento", o culpado não é o hardware nem o MariaDB, mas um punhado de consultas mal indexadas. O caminho é sempre o mesmo: ligar o slow query log, ranquear as consultas que mais custam, entender o plano com ANALYZE (que mostra o que aconteceu de verdade, não só a estimativa), e corrigir com o índice certo ou reescrevendo a consulta. No fim do artigo tem um checklist para você seguir hoje mesmo.
Primeiro, a regra: meça antes de mexer
Ao longo da minha experiência com banco de dados, vi muita empresa trocar servidor, aumentar innodb_buffer_pool_size no chute e até migrar de banco por causa de lentidão, quando o problema era uma única consulta rodando milhares de vezes por minuto sem índice.
Ajustar parâmetro sem saber qual consulta está pesando é como trocar o motor do carro por causa de um pneu murcho. Então a ordem é esta:
- Achar as consultas que mais consomem tempo no total (não só as mais lentas).
- Entender por que cada uma está lenta.
- Corrigir e medir de novo.
Bora fazer isso passo a passo.
Passo 1: ligue o slow query log
O slow query log registra toda consulta que passar de um tempo-limite. Dá para ligar sem reiniciar o servidor:
-- Liga o log de consultas lentas em tempo de execução
SET GLOBAL slow_query_log = 1;
-- Tempo-limite em segundos (comece com 1 e vá baixando para 0.5, 0.2...)
SET GLOBAL long_query_time = 1;
-- Recurso do MariaDB: grava o plano de execução junto com a consulta
SET GLOBAL log_slow_verbosity = 'query_plan,explain';
-- Opcional: registra também consultas que não usam índice (gera volume!)
SET GLOBAL log_queries_not_using_indexes = 1;
Atenção: long_query_time alterado com SET GLOBAL só vale para conexões novas. Se a sua aplicação usa pool de conexões, pode levar um tempo até tudo passar a ser registrado.
Para deixar permanente, coloque no arquivo de configuração:
[mariadb]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time = 1
log_slow_verbosity = query_plan,explain
Curiosidade útil: a partir do 10.11, essas variáveis ganharam nomes novos e mais coerentes (log_slow_query, log_slow_query_file, log_slow_query_time). Os nomes antigos continuam funcionando como apelidos, por isso usei eles aqui: confira a disponibilidade e os apelidos na versão que você usa.
O diferencial está no log_slow_verbosity = explain. No MariaDB, o log já traz o EXPLAIN de cada consulta lenta. Você não precisa reproduzir a consulta para ver o plano que ela usou naquele momento.
Passo 2: veja o que está rodando agora
Se o sistema está lento neste momento, não dá para esperar o log encher. Veja quem está rodando:
-- Lista as conexões e o que cada uma está executando
SHOW FULL PROCESSLIST;
Achou uma consulta rodando há minutos? A partir do 10.9, o MariaDB deixa você ver o plano real dela enquanto ela ainda executa, com contadores de linhas lidas até ali:
-- Substitua 1234 pelo Id que aparece no PROCESSLIST
SHOW ANALYZE FOR 1234;
Isso é ouro em incidente: você descobre por que a consulta está travada sem precisar matá-la e rodar de novo.
Passo 3: ranqueie o que pesa no total
Uma consulta de 8 segundos que roda uma vez por dia incomoda menos do que uma de 200 ms que roda 50 mil vezes por hora. Por isso você precisa agrupar o log por "formato" de consulta e somar o tempo.
Duas ferramentas resolvem:
# Já vem com o MariaDB: top 10 por tempo agregado (confira o efeito do lock time)
mariadb-dumpslow -s t -t 10 /var/log/mysql/mariadb-slow.log
# Mais completa (Percona Toolkit): relatório por impacto
pt-query-digest /var/log/mysql/mariadb-slow.log > relatorio-lentas.txt
O pt-query-digest entrega um ranking com tempo total, número de execuções, linhas examinadas e linhas retornadas. Olhe com carinho para a razão linhas examinadas ÷ linhas retornadas: se a consulta lê 2 milhões de linhas para devolver 20, achamos um candidato.
Passo 4: EXPLAIN mostra o plano, ANALYZE mostra a verdade
Vamos a um caso típico. Uma tela de "pedidos pagos do dia" que ficou lenta conforme a tabela cresceu:
SELECT id, cliente_id, total
FROM pedidos
WHERE DATE(criado_em) = '2026-11-03'
AND status = 'pago'
ORDER BY criado_em DESC;
O EXPLAIN mostra o que o otimizador pretende fazer, com base em estimativas. O ANALYZE do MariaDB vai além: ele executa a consulta e coloca, lado a lado com a estimativa, o que aconteceu de verdade.
ANALYZE SELECT id, cliente_id, total
FROM pedidos
WHERE DATE(criado_em) = '2026-11-03'
AND status = 'pago'
ORDER BY criado_em DESC;
As colunas que importam são estas (valores ilustrativos):
| Coluna | Valor | O que significa |
|---|---|---|
type |
ALL |
Leu a tabela inteira (full scan) |
rows |
2.100.000 | Quantas linhas o otimizador estimou |
r_rows |
2.400.000 | Quantas linhas ele leu de verdade |
r_filtered |
0.04 | Só 0,04% das linhas lidas eram úteis |
Extra |
Using where; Using filesort |
Filtrou depois de ler e ainda ordenou em memória ou disco |
Para mais detalhe, inclusive tempo gasto em cada etapa, use ANALYZE FORMAT=JSON SELECT ... e procure os campos r_total_time_ms.
Atenção: ANALYZE executa a instrução de verdade. Até um SELECT pode consumir bastante CPU e I/O, adquirir locks conforme a consulta ou chamar funções com efeitos colaterais. ANALYZE UPDATE e ANALYZE DELETE alteram dados: prefira EXPLAIN em produção e faça a medição real numa cópia de teste. Uma transação seguida de ROLLBACK só desfaz alterações transacionais; não desfaz escritas em tabelas não transacionais, inclusive as feitas por triggers, nem efeitos externos. Mesmo com InnoDB, há locks e custo de execução e rollback. Não trate ROLLBACK como botão de “teste sem risco”.
Leitura rápida dos sintomas mais comuns:
| O que você vê | Causa provável | Correção |
|---|---|---|
type = ALL com r_rows alto |
Falta de índice, ou índice que não pode ser usado | Criar o índice certo ou reescrever o filtro |
rows muito diferente de r_rows |
Estatísticas desatualizadas | ANALYZE TABLE (veja abaixo) |
r_filtered muito baixo |
Índice pouco seletivo | Índice composto com a coluna mais seletiva |
Using filesort em muitas linhas |
ORDER BY sem índice compatível |
Incluir a coluna do ORDER BY no índice |
Using temporary |
GROUP BY / DISTINCT sem índice |
Índice nas colunas agrupadas |
No nosso exemplo, vale conferir a versão antes de culpar o DATE(). A partir do MariaDB 11.1, comparações de DATE(coluna) e YEAR(coluna) com constantes podem aproveitar índices sobre colunas DATE, DATETIME ou TIMESTAMP: o otimizador transforma essas condições em intervalos. Para versões anteriores, e para deixar o intervalo explícito, a reescrita abaixo continua sendo uma boa opção. O índice composto combina a igualdade em status com o intervalo e a ordenação por criado_em:
-- Índice composto: igualdade primeiro, intervalo/ordenação depois
ALTER TABLE pedidos ADD INDEX idx_status_criado (status, criado_em);
-- Filtro reescrito como intervalo, sem função na coluna
SELECT id, cliente_id, total
FROM pedidos
WHERE status = 'pago'
AND criado_em >= '2026-11-03'
AND criado_em < '2026-11-04'
ORDER BY criado_em DESC;
Rode o ANALYZE de novo e compare. Com esse índice, o otimizador pode usar acesso por intervalo e entregar as linhas na ordem pedida sem filesort, mas a escolha depende dos dados, das estatísticas e do custo estimado. O resultado esperado é ler menos linhas; confirme isso no plano e nos tempos, em vez de assumir que aconteceu.
Passo 5: por que o otimizador escolheu esse plano?
Às vezes o índice existe e o MariaDB mesmo assim não usa. Antes de sair forçando FORCE INDEX, pergunte ao otimizador o que ele pensou:
SET optimizer_trace = 'enabled=on';
SELECT id, cliente_id, total
FROM pedidos
WHERE status = 'pago' AND criado_em >= '2026-11-03';
-- O raciocínio completo do otimizador, em JSON
SELECT TRACE FROM information_schema.OPTIMIZER_TRACE\G
SET optimizer_trace = 'enabled=off';
No trace, procure as seções rows_estimation (quantas linhas ele achou que cada índice traria) e considered_execution_plans (os planos que ele comparou e o custo de cada um). Quase sempre o motivo é estimativa errada, o que nos leva ao próximo ponto.
Estatísticas: o otimizador só é bom quanto os números que ele tem
Se o rows do ANALYZE está muito longe do r_rows, o otimizador está decidindo com informação ruim. O MariaDB tem estatísticas independentes de engine, incluindo histogramas, que dizem como os valores estão distribuídos em uma coluna e não só quantos valores distintos existem:
-- Coleta estatísticas persistentes; pode ser caro em tabelas grandes
ANALYZE TABLE pedidos PERSISTENT FOR ALL;
-- Ou apenas para colunas específicas (mais leve em tabelas grandes)
ANALYZE TABLE pedidos PERSISTENT FOR COLUMNS (status, criado_em) INDEXES ();
Isso faz muita diferença em colunas desbalanceadas. Imagine status, onde 95% dos pedidos estão como entregue e 0,1% como estornado. Com histograma, o otimizador sabe que filtrar por estornado é muito seletivo e que filtrar por entregue não é.
Também vale saber: a partir do MariaDB 11.0, o otimizador passou a usar um modelo de custo novo, calibrado para SSD e com custos mais realistas para cada operação. Se você está em 10.x e sofre com planos ruins em consultas complexas, isso é mais um argumento para planejar o upgrade (veja o artigo sobre o fim de vida do 10.6).
As armadilhas que resolvem 80% dos casos
Estes são os padrões que mais encontro em auditoria de banco:
- Função na coluna do filtro: pode dificultar o uso do índice, mas não é uma regra absoluta. Desde o 11.1,
DATE()eYEAR()têm otimizações para comparações com constantes sobre colunas de data indexadas. Confira a versão e o plano; quando necessário, reescreva como intervalo ou use uma coluna virtual indexada. - Conversão implícita de tipo:
WHERE cpf = 12345678900comcpfsendoVARCHAR. O número força conversão linha a linha e o índice é ignorado. UseWHERE cpf = '12345678900'. - Ordem errada no índice composto: colunas de igualdade primeiro, depois a de intervalo ou ordenação. Um índice
(criado_em, status)não ajuda o nosso exemplo tanto quanto(status, criado_em). LIKE '%texto': curinga no início impede índice B-tree. Para busca textual, use índiceFULLTEXT.- Paginação com
OFFSETgigante:LIMIT 20 OFFSET 500000lê e descarta 500 mil linhas. Troque por paginação por chave:
-- Em vez de OFFSET, continue a partir do último id da página anterior
SELECT id, cliente_id, total
FROM pedidos
WHERE id < 987654 -- último id exibido na página anterior
ORDER BY id DESC
LIMIT 20;
Para um campo dentro de JSON, o MariaDB permite criar uma coluna virtual e indexá-la. O valor virtual não é armazenado como uma coluna comum na linha, mas o índice ocupa espaço em disco e precisa ser mantido nas escritas. Não é índice de graça:
-- Coluna virtual: sem valor persistido na linha; o índice ocupa espaço
ALTER TABLE clientes
ADD COLUMN cpf_json VARCHAR(14)
AS (JSON_VALUE(dados, '$.cpf')) VIRTUAL,
ADD INDEX idx_cpf_json (cpf_json);
-- Agora a busca usa índice
SELECT id, nome FROM clientes WHERE cpf_json = '123.456.789-00';
Dois recursos do MariaDB que pouca gente conhece
1. Descobrir índices que ninguém usa. Índice demais também deixa o banco lento, porque cada INSERT e UPDATE precisa atualizar todos eles. O MariaDB tem estatísticas de uso nativas:
-- Liga a coleta de estatísticas de uso (overhead baixo)
SET GLOBAL userstat = 1;
-- Depois de alguns dias de uso normal: quais índices foram lidos?
SELECT table_schema, table_name, index_name, rows_read
FROM information_schema.INDEX_STATISTICS
ORDER BY rows_read DESC;
Índice que não aparece nessa lista depois de um ciclo completo de uso (inclua o fechamento do mês!) é candidato à remoção.
2. Testar a remoção sem remover. Apagar um índice de uma tabela grande e descobrir que alguma consulta dependia dele é um pesadelo, porque recriar leva tempo. Desde o 10.6, o MariaDB tem índices ignorados: o índice continua existindo e sendo atualizado, mas o otimizador finge que ele não está lá.
-- O otimizador passa a ignorar o índice (operação instantânea)
ALTER TABLE pedidos ALTER INDEX idx_antigo IGNORED;
-- Algo piorou? Volte na hora, sem reconstruir nada
ALTER TABLE pedidos ALTER INDEX idx_antigo NOT IGNORED;
Deixe o índice ignorado por alguns dias. Se nada piorar, aí sim faça o DROP INDEX com segurança.
O que isso significa para você
Checklist para aplicar hoje:
- Ligue o slow query log com
log_slow_verbosity = query_plan,explain. - Depois de 24 a 48 horas, rode o
pt-query-digeste pegue as 5 consultas com maior tempo total. - Para cada uma, rode
ANALYZEe comparerowscomr_rows. - Corrija com índice composto, reescrita do filtro ou atualização de estatísticas.
- Meça de novo. Se o tempo total da consulta não caiu, a correção não funcionou.
- Uma vez por trimestre, revise índices sem uso com
userstate teste a remoção comIGNORED.
E um lembrete: antes de mexer em índice de tabela grande em produção, garanta que você tem um backup que restaura de verdade. Falo disso em detalhe no artigo sobre backup sem parar o banco com mariadb-backup.
Quer ir mais fundo no motor? Veja como o MariaDB lida com milhares de conexões e como uma única correção deixou o 11.8 até 3x mais rápido:
Leia também
- Como o MariaDB 11.8 ficou até 3x mais rápido em cargas em memória
- MariaDB: thread pool gratuito para milhares de conexões
- MariaDB 10.6 chegou ao fim de vida: para qual versão migrar?
- Storage engines do MariaDB: o banco que tem vários motores debaixo do capô
- MariaDB ColumnStore: banco colunar para análise sem sair do SQL
- Até onde o MariaDB aguenta? As perguntas que todo CTO faz
Todos os artigos sobre MariaDB →
Referências oficiais
Nota técnica: os exemplos foram revisados com a documentação, mas não executados em um servidor MariaDB nesta publicação. Valide versão, caminhos, privilégios e comportamento em laboratório antes de usar em produção. As capas são ilustrações fotográficas geradas por IA, não resultados de testes.
Continue essa conversa
Quer trocar experiências sobre MariaDB? Entre para a Comunidade MariaDB Brasil.
Me acompanhe no LinkedIn e no Instagram. Para fortalecer sua base em SQL, conheça meu livro Aprendendo MySQL. Conheça também a HTI Tecnologia.