Este artigo explora técnicas de otimização de consultas no MySQL, com foco na criação e uso de índices para melhorar o desempenho. Analisaremos diferentes cenários e estratégias de otimização.
Cenário Inicial e Consulta Lenta
Consideremos um banco de dados MySQL 5.6 com as seguintes tabelas:
Course(Curso): 100 registrosStudent(Aluno): 70.000 registrosSC(Notas de Alunos): 700.000 registros
O objetivo é encontrar os alunos que tiraram 100 em "Português". A consulta inicial é:
SELECT s.*
FROM Student s
WHERE s.s_id IN (SELECT s_id FROM SC sc WHERE sc.c_id = 0 AND sc.score = 100);
O tempo de execução desta consulta foi de 30248.271 segundos.
Análise do Plano de Execução e Criação de Índices
Ao analisar o plano de execução com EXPLAIN, observamos que nenhum índice foi utilizado (type: ALL). A primeira etapa da otimização é criar índices nas colunas usadas nas cláusulas WHERE.
Criamos índices nas colunas c_id e score da tabela SC:
CREATE INDEX sc_c_id_index ON SC(c_id);
CREATE INDEX sc_score_index ON SC(score);
Após a criação dos índices, a mesma consulta levou apenas 1.054 segundos, uma melhoria de mais de 30.000 vezes.
Otimização Adicional: JOIN vs. Subconsulta
Apesar da melhoria significativa, 1 segundo ainda pode ser considerado longo. Analisando o plano de execução otimizado, percebemos que o MySQL pode transformar a subconsulta em uma cláusula EXISTS com subconsultas dependentes (DEPENDENT SUBQUERY), o que pode levar à execução externa primeiro, seguida pela interna em loop, resultando em ineficiência.
Vamos testar a reescrita da consulta usando um INNER JOIN:
SELECT s.*
FROM Student s
INNER JOIN SC sc ON sc.s_id = s.s_id
WHERE sc.c_id = 0 AND sc.score = 100;
Inicialmente, removendo os índices criados anteriormente para reavaliar, esta consulta demorou 0.057 segundos. A análise do plano de execução indicou a necessidade de um índice na coluna s_id da tabela SC para otimizar o JOIN.
Criando o índice sc_s_id_index:
CREATE INDEX sc_s_id_index ON SC(s_id);
A consulta JOIN, após a criação do índice sc_s_id_index, levou 1.076 segundos, o que é mais lento. Isso sugere que o otimizador do MySQL pode ter uma estratégia mais eficiente por padrão.
Para garantir que a filtragem ocorra antes da junção (especialmente em tabelas grandes), podemos reescrever a consulta explicitamente:
SELECT s.*
FROM (
SELECT *
FROM SC sc
WHERE sc.c_id = 0 AND sc.score = 100
) t
INNER JOIN Student s ON t.s_id = s.s_id;
Com os índices sc_c_id_index e sc_score_index já existentes, esta consulta otimizada levou apenas 0.001 segundos.
O plano de execução mostra que a subconsulta (filtragem na tabela SC) é executada primeiro, utilizando os índices, seguida pela junção com a tabela Student.
Otimização com Índices Compostos
Em um cenário com dados de produção maiores (3 milhões de registros na tabela SC) e distribuições de dados mais dispersas, a eficiência pode diminuir. Testando novamente:
SELECT s.*
FROM Student s
INNER JOIN SC sc ON sc.s_id = s.s_id
WHERE sc.c_id = 81 AND sc.score = 84;
Esta consulta levou 0.061 segundos. A análise do plano de execução mostrou a operação INTERSECT, indicando que o MySQL estava usando dois índices separadamente e combinando os resultados.
Quando a cardinalidade de colunas individuais não é alta, mas a combinação delas é, um índice composto é mais eficiente. Removemos os índices individuais e criamos um índice composto:
ALTER TABLE SC DROP INDEX sc_c_id_index;
ALTER TABLE SC DROP INDEX sc_score_index;
CREATE INDEX sc_c_id_score_index ON SC(c_id, score);
A mesma consulta agora leva 0.007 segundos, uma melhoria significativa.
Otimização de Índices Adicionais
Índices de Coluna Única vs. Compostos
Para uma consulta como:
SELECT * FROM user_test_copy WHERE sex = 2 AND type = 2 AND age = 10;
Com índices de coluna única em sex, type e age (3 milhões de registros), a consulta levou 0.415 segundos. O plano de execução mostrou INDEX_MERGE.
Criando um índice composto:
CREATE INDEX user_test_index_sex_type_age ON user_test(sex, type, age);
A mesma consulta agora leva 0.032 segundos, mais de 10 vezes mais rápido. A eficiência aumenta com a cardinalidade do índice composto.
Princípio da "Menor Chave à Esquerda" (Leftmost Key Prefix)
Um índice composto como (sex, type, age) pode ser utilizado para consultas que filtram apenas por sex, ou por sex e type, ou por sex, type e age. O primeiro campo do índice (sex) deve estar presente na cláusula WHERE para que o índice seja utilizado eficientemente.
Índices de Cobertura (Covering Indexes)
Quando todos os campos necessários para a consulta estão incluídos no índice, o MySQL não precisa acessar a tabela principal, acelerando a recuperação dos dados.
SELECT sex, type, age
FROM user_test
WHERE sex = 2 AND type = 2 AND age = 10;
Com o índice composto (sex, type, age), esta consulta levou 0.003 segundos, muito mais rápido do que selecionar todos os campos.
Otimização de Ordenação (ORDER BY)
Para melhorar o desempenho de ordenação:
SELECT *
FROM user_test
WHERE sex = 2 AND type = 2
ORDER BY user_name;
Esta consulta levou 0.139 segundos. Criar um índice na coluna de ordenação (user_name) pode otimizar esta operação.
Resumo das Estratégias de Otimização
- Prefira tipos de dados numéricos e com o menor comprimento possível.
- Utilize índices de coluna única quando apropriado.
- Crie índices compostos quando a combinação de colunas aumentar significativamente a cardinalidade e a eficiência da consulta.
- Implemente índices de cobertura para consultas que selecionam um subconjunto de colunas já indexadas.
- Crie índices nas colunas usadas em junções (
JOIN). - Crie índices nas colunas usadas em cláusulas
WHERE. - Crie índices nas colunas usadas em
ORDER BYeGROUP BY. - Evite o uso de funções em colunas nas cláusulas
WHERE, pois isso pode invalidar o uso de índices. - Analise sempre o plano de execução (
EXPLAIN) para entender como o MySQL está executando suas consultas e identificar gargalos.