Aula 13: Índices no MySQL — como criar e quando usar para performance
Bem-vindo à Aula 13 do curso MySQL — Do Zero ao Avançado. Hoje, 19 de junho de 2026, vamos mergulhar em um dos pilares mais críticos para qualquer profissional que trabalha com bancos de dados: os índices no MySQL. Se você já construiu tabelas e escreveu consultas SELECT, JOIN e WHERE, sabe que a diferença entre uma query que responde em milissegundos e outra que paralisa o servidor por minutos está, quase sempre, na correta utilização de índices. Índices são estruturas auxiliares que permitem ao MySQL localizar linhas sem precisar varrer a tabela inteira — é a diferença entre consultar o índice remissivo de um livro ou folhear todas as suas páginas uma a uma.
Nesta aula, você compreenderá profundamente como os índices funcionam internamente, quais tipos o MySQL oferece, e principalmente, quando e como criar cada um deles para obter ganhos reais de performance. Não se trata apenas de decorar a sintaxe do comando CREATE INDEX; precisamos entender o impacto em operações de leitura e escrita, como o otimizador do MySQL decide usar ou não um índice e quais armadilhas podem transformar um índice bem-intencionado em um pesadelo de degradação de performance. Em nossos projetos na JRT Technology Solutions, frequentemente somos chamados para diagnosticar sistemas lentos, e em mais de 80% dos casos, a raiz do problema está em índices ausentes ou mal dimensionados.
O conteúdo que preparamos é progressivo: começaremos com a base teórica necessária sobre estruturas B-Tree e Hash, passaremos para a criação prática de índices primários, únicos, comuns, de texto completo e espaciais, e culminaremos no uso avançado do comando EXPLAIN para analisar e validar se seus índices estão realmente sendo utilizados pelo otimizador. Cada conceito será ilustrado com exemplos práticos executáveis no seu ambiente local. Você aprenderá também a identificar gargalos usando SHOW INDEX, a interpretar colunas como cardinalidade e seletividade, e a entender por que um índice perfeito no papel pode ser ignorado pelo MySQL em tempo de execução.
Esta aula tem nível intermediário porque pressupõe que você já domina a criação de tabelas, chaves primárias e estrangeiras, e a escrita de consultas com WHERE, ORDER BY, GROUP BY e JOINs — tópicos que cobrimos em profundidade nas Aulas 6 a 12. Ao final desta aula, você será capaz de auditar suas próprias tabelas, identificar colunas candidatas a indexação, criar índices que realmente aceleram suas queries críticas e evitar os principais erros que desperdiçam espaço em disco e penalizam operações de INSERT, UPDATE e DELETE. Pegue seu terminal, conecte-se ao seu servidor MySQL de testes e vamos dominar juntos o fascinante mundo dos índices.
O que você vai aprender nesta aula
- Compreender o funcionamento interno de índices no MySQL — estruturas B-Tree, Hash e Full-Text
- Diferenciar os tipos de índices disponíveis: PRIMARY KEY, UNIQUE, INDEX (não único), FULLTEXT e SPATIAL
- Criar, alterar e remover índices utilizando CREATE INDEX, ALTER TABLE … ADD INDEX e DROP INDEX
- Projetar índices compostos (multicoluna) seguindo a regra da coluna mais seletiva primeiro
- Utilizar o comando EXPLAIN para verificar se um índice está sendo usado e interpretar o plano de execução
- Monitorar e auditar índices existentes com SHOW INDEX e INFORMATION_SCHEMA
- Identificar e corrigir erros comuns que tornam índices ineficazes ou prejudiciais
Pré-requisitos e Ambiente
Para acompanhar esta aula com máximo proveito, você precisará de um servidor MySQL 8.0 ou superior em funcionamento. Assumimos que você já possui o MySQL instalado e configurado (Aulas 2 e 3), sabe criar bancos de dados e tabelas (Aulas 6 e 7), e está confortável escrevendo consultas com WHERE, JOIN e ORDER BY (Aulas 10, 11 e 12). Utilizaremos o banco de dados de demonstração curso_mysql que construímos ao longo do curso. Recomendamos que você execute todos os comandos em um ambiente de testes, pois faremos inserções em massa para demonstrar o impacto de índices na prática. Teremos dois terminais abertos: um com o cliente mysql conectado e outro para comandos do sistema operacional, se necessário. Todos os exemplos foram testados em Ubuntu 24.04 LTS e Rocky Linux 9, com MySQL 8.0.37 — as saídas podem variar ligeiramente em versões diferentes, mas os conceitos permanecem idênticos.
Entendendo Índices no MySQL — Estrutura Interna e Tipos
Quando falamos em índices no MySQL, estamos nos referindo a estruturas de dados auxiliares que o motor de armazenamento (normalmente o InnoDB) mantém para acelerar a localização de registros. Imagine uma tabela clientes com 2 milhões de linhas e uma consulta como SELECT * FROM clientes WHERE cpf = '123.456.789-00'. Sem índice, o MySQL precisa ler cada uma das 2 milhões de linhas sequencialmente — é o temido full table scan. Com um índice na coluna cpf, o MySQL pode navegar por uma estrutura de árvore balanceada (B-Tree) e localizar o registro em 3 ou 4 saltos, uma diferença brutal de desempenho.
Por padrão, o InnoDB organiza fisicamente os dados em uma estrutura chamada índice clusterizado (clustered index), que é construído sobre a chave primária da tabela. Isso significa que as linhas da tabela são armazenadas nas folhas da B-Tree da PRIMARY KEY — entenda isso como se o conteúdo da tabela e o índice primário fossem a mesma estrutura. Todos os demais índices são chamados de índices secundários (secondary indexes) e armazenam em suas folhas uma cópia do valor da chave primária correspondente, e não o endereço físico da linha. Essa compreensão é vital: ao buscar por um índice secundário, o MySQL primeiro encontra o índice, obtém a PRIMARY KEY associada e depois acessa o índice clusterizado — processo conhecido como lookup duplo.
Os índices no MySQL podem ser de vários tipos conforme sua finalidade. O PRIMARY KEY é o índice primário e clusterizado (no InnoDB), que garante unicidade e não permite NULLs. O UNIQUE INDEX também garante unicidade, mas permite NULLs e cria um índice secundário. O INDEX simples (ou não único) acelera consultas sem impor restrições de unicidade. Índices FULLTEXT são especializados para buscas em campos de texto longo com linguagem natural e modo booleano. Já os índices SPATIAL (ou R-Tree) otimizam consultas sobre dados geoespaciais — este último disponível apenas para tabelas MyISAM em versões antigas, mas no MySQL 8.0 o InnoDB também oferece suporte a índices espaciais. Na prática diária, você trabalhará 95% do tempo com PRIMARY KEY, UNIQUE e INDEX simples, mas é importante conhecer todo o arsenal.
Internamente, a B-Tree do InnoDB é uma árvore balanceada onde cada página (geralmente 16KB) contém múltiplas chaves e ponteiros. Isso minimiza o número de operações de leitura em disco, mantendo a profundidade da árvore baixa. Para tabelas com centenas de milhões de registros, uma B-Tree típica tem 4 ou 5 níveis de profundidade. Já os índices Hash estariam disponíveis teoricamente no motor MEMORY, mas seu uso moderno é raro — focaremos na B-Tree, que é a espinha dorsal de praticamente tudo no InnoDB. Com essa base teórica clara, podemos passar à criação prática, onde a teoria se transforma em ganhos mensuráveis de performance.
Criando Índices no MySQL na Prática — Sintaxe Completa e Exemplos Reais
Vamos construir um cenário realista de laboratório. Conecte-se ao seu MySQL e execute o script a seguir para criar um banco de testes e popular uma tabela com volume suficiente para sentirmos a diferença dos índices. Vamos gerar 500 mil registros utilizando um procedimento armazenado simples — essa massa de dados tornará os tempos de resposta facilmente perceptíveis mesmo em máquinas modestas.
Passo 1: Acesse o MySQL e crie o banco de dados da aula.
-- Conecte-se ao MySQL como root ou usuário com privilégios CREATE DATABASE
mysql -u root -p
/* ============================================================
Aula 13 — Criando base de testes para Índices no MySQL
============================================================ */
-- Criando o banco de dados dedicado para a aula
CREATE DATABASE IF NOT EXISTS aula13_indices
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- Selecionando o banco recém-criado
USE aula13_indices;
-- Criando tabela de vendas SEM índices adicionais (apenas a PK)
CREATE TABLE vendas (
id BIGINT AUTO_INCREMENT,
data_venda DATE NOT NULL,
id_cliente INT NOT NULL,
id_produto INT NOT NULL,
quantidade INT NOT NULL DEFAULT 1,
valor_unitario DECIMAL(10, 2) NOT NULL,
valor_total DECIMAL(12, 2) GENERATED ALWAYS AS (quantidade * valor_unitario) STORED,
status ENUM('pendente', 'aprovada', 'cancelada') NOT NULL DEFAULT 'pendente',
observacao TEXT,
PRIMARY KEY (id)
) ENGINE=InnoDB;
Acabamos de criar uma tabela vendas com apenas a chave primária como índice. As colunas id_cliente, id_produto, data_venda e status — que certamente serão usadas em filtros — estão completamente desindexadas neste momento. Essa é exatamente a situação que encontramos em sistemas reais que cresceram sem planejamento de índices.
Passo 2: Vamos popular a tabela com 500 mil registros usando um procedimento que insere dados pseudoaleatórios.
-- Alterando delimitador para criar procedure
DELIMITER //
CREATE PROCEDURE popular_vendas(IN total_registros INT)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE data_base DATE DEFAULT '2024-01-01';
WHILE i < total_registros DO
INSERT INTO vendas (data_venda, id_cliente, id_produto, quantidade, valor_unitario, status, observacao)
VALUES (
DATE_ADD(data_base, INTERVAL FLOOR(RAND() * 730) DAY),
FLOOR(1 + RAND() * 5000),
FLOOR(1 + RAND() * 800),
FLOOR(1 + RAND() * 20),
ROUND(10 + RAND() * 990, 2),
ELT(FLOOR(1 + RAND() * 3), 'pendente', 'aprovada', 'cancelada'),
CONCAT('Venda registrada automaticamente - ciclo ', i)
);
SET i = i + 1;
-- Commit a cada 10.000 linhas para não estourar buffer
IF i % 10000 = 0 THEN
COMMIT;
END IF;
END WHILE;
COMMIT;
END //
DELIMITER ;
-- Executando o procedimento para gerar 500.000 registros
CALL popular_vendas(500000);
-- Verificando quantos registros foram inseridos
SELECT COUNT(*) AS total_registros FROM vendas;
+-----------------+
| total_registros |
+-----------------+
| 500000 |
+-----------------+
1 row in set (0.01 sec)
Temos agora meio milhão de vendas na tabela. Vamos testar a performance de uma consulta típica que um sistema de ERP ou e-commerce faria: buscar vendas de um cliente específico em um determinado período. Ative o perfil de tempo de execução para medir exatamente o impacto.
-- Habilitando profiling para medir tempo de execução (MySQL 8.0)
SET profiling = 1;
-- Consulta sem índice: buscar vendas do cliente 725 no primeiro trimestre de 2025
SELECT id, data_venda, id_produto, quantidade, valor_total, status
FROM vendas
WHERE id_cliente = 725
AND data_venda BETWEEN '2025-01-01' AND '2025-03-31'
ORDER BY data_venda DESC;
-- Exibindo profile da consulta
SHOW PROFILES;
+----------+------------+----------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+----------------------------------------------------------------------------------------------+
| 1 | 0.45280750 | SELECT id, data_venda, ... FROM vendas WHERE id_cliente = 725 AND data_venda BETWEEN ... |
+----------+------------+----------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
0,45 segundos — em meio milhão de linhas. Pode parecer aceitável, mas quando essa consulta é executada centenas de vezes por minuto sob carga concorrente, o impacto é devastador. E lembre-se: 500 mil linhas é uma tabela pequena para padrões de produção. Agora, vamos criar nosso primeiro índice direcionado e medir a diferença.
Passo 3: Criando um índice composto nas colunas id_cliente e data_venda.
-- Criando índice composto: id_cliente + data_venda
-- A ordem importa: coluna de igualdade primeiro, coluna de range depois
CREATE INDEX idx_vendas_cliente_data ON vendas (id_cliente, data_venda);
-- Verificando os índices da tabela
SHOW INDEX FROM vendas;
+--------+------------+---------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+---------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| vendas | 0 | PRIMARY | 1 | id | A | 498200 | NULL | NULL | | BTREE | | | YES | NULL |
| vendas | 1 | idx_vendas_cliente_data | 1 | id_cliente | A | 4842 | NULL | NULL | | BTREE | | | YES | NULL |
| vendas | 1 | idx_vendas_cliente_data | 2 | data_venda | A | 481693 | NULL | NULL | | BTREE | | | YES | NULL |
+--------+------------+---------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
3 rows in set (0.00 sec)
A saída do SHOW INDEX nos revela informações preciosas. O índice idx_vendas_cliente_data é do tipo BTREE, possui duas colunas na sequência que definimos, e o campo Cardinality (cardinalidade) nos dá uma estimativa de quantos valores distintos existem em cada nível do índice. Observe que id_cliente tem cardinalidade aproximada de 4.842 (coerente com 5.000 clientes possíveis no nosso script), e a combinação com data_venda salta para quase 482 mil — ou seja, a combinação das duas colunas é altamente seletiva, exatamente o que buscamos. Vamos agora repetir a mesma consulta e conferir o ganho.
-- Repetindo a consulta anterior (agora com índice)
SELECT id, data_venda, id_produto, quantidade, valor_total, status
FROM vendas
WHERE id_cliente = 725
AND data_venda BETWEEN '2025-01-01' AND '2025-03-31'
ORDER BY data_venda DESC;
SHOW PROFILES;
+----------+------------+----------------------------------------------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+----------------------------------------------------------------------------------------------+
| 1 | 0.45280750 | ... (execução sem índice) |
| 2 | 0.00112500 | SELECT id, data_venda, ... FROM vendas WHERE id_cliente = 725 AND data_venda BETWEEN ... |
+----------+------------+----------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
De 0,45 segundos para 0,0011 segundos — um ganho de aproximadamente 400 vezes! Isso é o poder de um índice bem projetado. O MySQL não precisou mais varrer a tabela inteira; ele navegou diretamente pela B-Tree, localizou o nó correspondente ao cliente 725 e percorreu sequencialmente as entradas dentro do range de datas especificado. Em nossos projetos na JRT Technology Solutions, é exatamente esse tipo de otimização que transforma sistemas lentos e reativos em plataformas escaláveis que suportam milhares de transações por segundo.
Índices Compostos e Cobertura — Maximizando a Performance
Um dos maiores equívocos que encontramos em equipes de desenvolvimento é a criação de múltiplos índices simples em cada coluna usada em WHERE, em vez de projetar índices compostos estratégicos. Índices no MySQL compostos (ou multicoluna) seguem o princípio do prefixo mais à esquerda: o MySQL pode utilizar o índice se a consulta filtrar pela primeira coluna, pelas duas primeiras, pelas três primeiras, e assim por diante — mas nunca "pulando" colunas. Nosso índice idx_vendas_cliente_data (id_cliente, data_venda) será utilizado em uma consulta que filtra apenas por id_cliente, mas será ignorado se filtrarmos apenas por data_venda (a menos que seja um index skip scan, recurso do MySQL 8.0 que discutiremos a seguir).
Uma técnica avançada e extremamente eficiente é o índice de cobertura (covering index). Ele ocorre quando todas as colunas que a consulta precisa — tanto no SELECT quanto no WHERE, JOIN, ORDER BY e GROUP BY — estão presentes no índice. Com isso, o MySQL pode satisfazer toda a consulta lendo apenas o índice secundário, sem nunca precisar acessar o índice clusterizado para buscar a linha completa. Esse é o "Santo Graal" da otimização: operações de leitura puramente sequenciais dentro da B-Tree do índice, sem saltos aleatórios para a tabela. Vamos criar um índice de cobertura para uma consulta frequente no nosso sistema fictício: o dashboard de vendas por produto e status.
-- Suponha que a query crítica do dashboard seja:
-- SELECT id_produto, status, COUNT(*), SUM(valor_total)
-- FROM vendas
-- WHERE data_venda >= '2025-06-01'
-- GROUP BY id_produto, status;
-- Criando um índice que cubra: data_venda (WHERE), id_produto e status (GROUP BY) e valor_total (agregação)
CREATE INDEX idx_vendas_dashboard ON vendas (data_venda, id_produto, status, valor_total);
-- Executando a consulta com EXPLAIN para verificar cobertura
EXPLAIN SELECT id_produto, status, COUNT(*), SUM(valor_total)
FROM vendas
WHERE data_venda >= '2025-06-01'
GROUP BY id_produto, status;
+----+-------------+--------+------------+-------+------------------------------------------+-----------------------+---------+------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+------------------------------------------+-----------------------+---------+------+--------+----------+--------------------------+
| 1 | SIMPLE | vendas | NULL | range | idx_vendas_dashboard,idx_vendas_cliente_data | idx_vendas_dashboard | 3 | NULL | 124998 | 100.00 | Using where; Using index |
+----+-------------+--------+------------+-------+------------------------------------------+-----------------------+---------+------+--------+----------+--------------------------+
1 row in set (0.00 sec)
Observe o valor "Using index" na coluna Extra. Isso é a assinatura do índice de cobertura: o MySQL está satisfazendo a consulta inteiramente a partir do índice, sem acessar os dados da tabela. A coluna type mostra range, indicando que o MySQL está percorrendo um intervalo da B-Tree (devido ao data_venda >= '2025-06-01'). A coluna rows estima ~125 mil linhas examinadas; com o índice correto, mesmo esse volume é processado de forma extremamente eficiente.
Projetar índices de cobertura demanda que você conheça profundamente as queries que sua aplicação executa. A ordem das colunas no índice deve ser: primeiro colunas usadas em condições de igualdade no WHERE, depois colunas de range, e então colunas do GROUP BY e ORDER BY. Colunas do SELECT podem vir por último, desde que isso não prejudique a seletividade do prefixo. Um erro comum é tentar cobrir todas as consultas com um único índice gigante — isso desperdiça espaço e penaliza escritas. Encontre um equilíbrio baseado no perfil de uso real da sua aplicação, algo que nossos especialistas da JRT Technology Solutions fazem utilizando ferramentas como o sys schema e Performance Schema do MySQL para identificar as queries mais custosas.
Analisando o Uso de Índices com EXPLAIN — O Raio-X das Suas Queries
Nenhuma discussão sobre índices no MySQL está completa sem dominar o comando EXPLAIN. Ele é o seu principal aliado para entender o que o otimizador está realmente fazendo com sua consulta. Vamos analisar em detalhes a saída do EXPLAIN, coluna por coluna, para que você possa auditar qualquer query e decidir com segurança se os índices estão cumprindo seu propósito.
| Coluna | Valores Comuns | Interpretação e Impacto |
|---|---|---|
| id | 1, 2, NULL | Identificador da etapa no plano. Queries com subconsultas ou UNION geram múltiplos ids. Etapas com mesmo id são executadas em paralelo. |
| select_type | SIMPLE, PRIMARY, SUBQUERY, DERIVED, UNION | Tipo de operação da linha. SIMPLE é uma consulta direta sem subselects. DERIVED indica subquery no FROM (tabela temporária). |
| table | Nome da tabela ou alias | Indica a tabela acessada. Pode ser <derivedN> para tabelas temporárias geradas por subqueries. |
| type | ALL, index, range, ref, eq_ref, const, system | O mais crítico: método de acesso. ALL = full table scan (RUIM). index = scan do índice (nem sempre bom). range = varredura de intervalo (aceitável). ref = busca por índice não único (BOM). eq_ref = busca por índice único em JOIN (ÓTIMO). const = busca por PRIMARY KEY ou UNIQUE com valor constante (EXCELENTE). |
| possible_keys | Lista de índices candidatos | Índices que o otimizador considerou usar. Se vazio, não há índice disponível para o filtro — grave sinal de alerta. |
| key | Nome do índice escolhido | Índice efetivamente usado. Se NULL, o otimizador optou por não usar índice (possível otimizador forçando table scan). |
| key_len | Tamanho em bytes da chave usada | Permite inferir quantas colunas do índice composto estão sendo efetivamente utilizadas. Ex: INT = 4 bytes, DATE = 3 bytes. |
| rows | Número estimado de linhas examinadas | Estimativa do otimizador. Valores altos em tabelas grandes indicam possível falta de índice seletivo. |
| Extra | Using index, Using where, Using temporary, Using filesort | "Using index" = índice de cobertura (desejável). "Using temporary" = tabela temporária criada (custo alto, repense GROUP BY / ORDER BY). "Using filesort" = ordenação em disco (evite com índice ordenado corretamente). |
Vamos testar cenários problemáticos para visualizar os sinais de alerta no EXPLAIN. Primeiro, uma consulta que força filesort — ou seja, o MySQL precisa ordenar resultados manualmente porque o índice não cobre a cláusula ORDER BY.
-- Consulta com filesort: ordenação por coluna que não está no índice após o range
EXPLAIN SELECT id_cliente, data_venda, valor_total
FROM vendas
WHERE id_cliente = 725
ORDER BY valor_total DESC;
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------------+
| 1 | SIMPLE | vendas | ref | idx_vendas_cliente_data | idx_vendas_cliente_data | 4 | const | 103 | 100.00 | Using where; Using filesort |
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------------+
Observe "Using filesort" no campo Extra. Embora o índice idx_vendas_cliente_data esteja sendo usado para localizar o cliente (type=ref, um bom sinal), a ordenação por valor_total — coluna que não faz parte do índice — força uma operação de ordenação à parte. Para evitar isso, precisaríamos de um índice que inclua valor_total na posição correta ou repensar a necessidade dessa ordenação específica.
Outro cenário clássico é a criação de um índice que o otimizador simplesmente ignora porque ele não o considera vantajoso — geralmente quando a cardinalidade é muito baixa. Vamos criar um índice na coluna status, que tem apenas 3 valores distintos, e ver o que acontece.
-- Criando índice em coluna de baixa cardinalidade (armadilha comum)
CREATE INDEX idx_vendas_status ON vendas (status);
-- Testando com EXPLAIN
EXPLAIN SELECT * FROM vendas WHERE status = 'aprovada';
+----+-------------+--------+------+-------------------+------+---------+------+--------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------+-------------------+------+---------+------+--------+----------+-------------+
| 1 | SIMPLE | vendas | ALL | idx_vendas_status | NULL | NULL | NULL | 498200 | 33.33 | Using where |
+----+-------------+--------+------+-------------------+------+---------+------+--------+----------+-------------+
Mesmo existindo o índice idx_vendas_status, o EXPLAIN mostra type=ALL e key=NULL — o otimizador preferiu fazer um full table scan. Por quê? Porque a coluna status tem apenas 3 valores, gerando baixíssima seletividade (aproximadamente 33% das linhas para cada valor). Nesses casos, varrer a tabela inteira sequencialmente é frequentemente mais rápido do que navegar pela B-Tree e depois pular para as páginas de dados. Essa é uma lição valiosa: índices em colunas de baixa cardinalidade raramente são úteis e ainda custam espaço e performance de escrita. Remova esse índice imediatamente:
-- Removendo índice ineficaz
DROP INDEX idx_vendas_status ON vendas;
Gerenciando e Monitorando Índices no MySQL — Comandos Essenciais
Administrar índices no MySQL vai muito além de criá-los e esquecê-los. É necessário monitorar seu uso, tamanho e impacto ao longo do tempo. O comando SHOW INDEX que já utilizamos é a porta de entrada, mas você pode extrair informações mais ricas consultando a INFORMATION_SCHEMA e tabelas do Performance Schema. A tabela a seguir resume os principais comandos administrativos e suas finalidades:
| Comando / Consulta | Finalidade | Exemplo de Uso |
|---|---|---|
| SHOW INDEX FROM tabela | Lista todos os índices de uma tabela com cardinalidade, tipo, colunas e visibilidade | SHOW INDEX FROM vendas; |
| CREATE INDEX nome ON tabela (colunas) | Cria índice simples ou composto. Aceita opções como USING BTREE/HASH, COMMENT, VISIBLE/INVISIBLE | CREATE INDEX idx_data ON vendas(data_venda) COMMENT 'Índice para relatórios diários'; |
| CREATE UNIQUE INDEX nome ON tabela (colunas) | Cria índice único, garantindo não haver valores duplicados nas colunas especificadas | CREATE UNIQUE INDEX uq_email ON usuarios(email); |
| ALTER TABLE tabela ADD INDEX nome (colunas) | Alternativa ao CREATE INDEX; mesma funcionalidade com sintaxe de alteração de schema | ALTER TABLE vendas ADD INDEX idx_valor (valor_total); |
| DROP INDEX nome ON tabela | Remove um índice. Libera espaço em disco e elimina overhead em escritas | DROP INDEX idx_data ON vendas; |
| ALTER TABLE tabela ALTER INDEX nome VISIBLE/INVISIBLE | Torna o índice invisível para o otimizador sem removê-lo — útil para testar impacto antes de dropar definitivamente | ALTER TABLE vendas ALTER INDEX idx_data INVISIBLE; |
| SELECT * FROM information_schema.STATISTICS WHERE TABLE_NAME='vendas'; | Versão SQL do SHOW INDEX, permite filtrar e fazer joins com outras tabelas do INFORMATION_SCHEMA | Obter cardinalidade e tamanho estimado de todos os índices do banco |
| ANALYZE TABLE tabela; | Atualiza as estatísticas de distribuição de chaves (cardinalidade), melhorando as decisões do otimizador | ANALYZE TABLE vendas; |
| OPTIMIZE TABLE tabela; | Reorganiza fisicamente a tabela e índices, recuperando espaço fragmentado. Trava a tabela — use com cautela | OPTIMIZE TABLE vendas; |
Uma funcionalidade pouco explorada mas extremamente útil introduzida no MySQL 8.0 são os índices invisíveis. Antes de remover um índice que você suspeita não estar sendo utilizado, você pode torná-lo invisível para o otimizador e observar o comportamento da aplicação durante alguns dias. Se nenhuma degradação for percebida, você pode dropar o índice com segurança. É uma prática que recomendamos fortemente em nossos clientes na JRT Technology Solutions durante projetos de otimização de bases legadas.
-- Tornando um índice invisível para teste
ALTER TABLE vendas ALTER INDEX idx_vendas_dashboard INVISIBLE;
-- Verificando visibilidade dos índices
SELECT INDEX_NAME, IS_VISIBLE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'aula13_indices'
AND TABLE_NAME = 'vendas'
AND INDEX_NAME = 'idx_vendas_dashboard';
-- Se a performance não for afetada, remova-o definitivamente
-- DROP INDEX idx_vendas_dashboard ON vendas;
-- Caso contrário, torne-o visível novamente:
-- ALTER TABLE vendas ALTER INDEX idx_vendas_dashboard VISIBLE;
Verificando os Índices e Testando a Configuração
Esta seção obrigatória garante que todos os conceitos aplicados até aqui estão funcionando conforme esperado. Execute a sequência de verificações abaixo no seu ambiente e confira se as saídas são compatíveis. O objetivo é criar uma auditoria completa dos índices da nossa tabela de testes, medir o espaço ocupado e validar que as queries estão utilizando os índices corretamente.
-- 1. Listar todos os índices da tabela vendas com detalhes de cardinalidade e tipo
SELECT
INDEX_NAME,
COLUMN_NAME,
SEQ_IN_INDEX,
INDEX_TYPE,
CARDINALITY,
NULLABLE,
IS_VISIBLE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'aula13_indices'
AND TABLE_NAME = 'vendas'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
+---------------------------+-------------+--------------+------------+-------------+----------+------------+
| INDEX_NAME | COLUMN_NAME | SEQ_IN_INDEX | INDEX_TYPE | CARDINALITY | NULLABLE | IS_VISIBLE |
+---------------------------+-------------+--------------+------------+-------------+----------+------------+
| idx_vendas_cliente_data | id_cliente | 1 | BTREE | 4850 | | YES |
| idx_vendas_cliente_data | data_venda | 2 | BTREE | 482691 | | YES |
| idx_vendas_dashboard | data_venda | 1 | BTREE | 482691 | | YES |
| idx_vendas_dashboard | id_produto | 2 | BTREE | 479874 | | YES |
| idx_vendas_dashboard | status | 3 | BTREE | 499112 | | YES |
| idx_vendas_dashboard | valor_total | 4 | BTREE | 498938 | | YES |
| PRIMARY | id | 1 | BTREE | 498200 | | YES |
+---------------------------+-------------+--------------+------------+-------------+----------+------------+
7 rows in set (0.00 sec)
-- 2. Obter o tamanho estimado de cada índice em MB
SELECT
index_name,
ROUND(SUM(stat_value * @@innodb_page_size) / (1024 * 1024), 2) AS size_mb
FROM mysql.innodb_index_stats
WHERE database_name = 'aula13_indices'
AND table_name = 'vendas'
AND stat_name = 'size'
GROUP BY index_name
ORDER BY size_mb DESC;
+---------------------------+---------+
| index_name | size_mb |
+---------------------------+---------+
| PRIMARY | 42.10 |
| idx_vendas_dashboard | 24.75 |
| idx_vendas_cliente_data | 16.30 |
+---------------------------+---------+
3 rows in set (0.01 sec)
-- 3. Verificar se o otimizador continua usando o índice correto após ANALYZE
ANALYZE TABLE vendas;
EXPLAIN SELECT id_cliente, data_venda, valor_total
FROM vendas
WHERE id_cliente = 1550
AND data_venda >= '2025-06-01'
ORDER BY data_venda;
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-----------------------+
| 1 | SIMPLE | vendas | ref | idx_vendas_cliente_data | idx_vendas_cliente_data | 7 | const,const | 45 | 100.00 | Using index condition |
+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-----------------------+
Se seus resultados bateram com os exibidos, o ambiente está corretamente configurado e os índices estão funcionais. A presença de "Using index condition" indica que o MySQL está aplicando a otimização Index Condition Pushdown (ICP), onde o próprio motor de armazenamento filtra as linhas usando o índice antes de enviá-las à camada SQL — mais um nível de eficiência que um bom índice proporciona.
Erros Comuns e Como Resolver
Durante a implementação e manutenção de índices no MySQL, uma série de problemas recorrentes podem desperdiçar recursos ou até mesmo degradar a performance. Listamos os quatro erros mais frequentes que encontramos em campo, com suas causas, sintomas e soluções completas.
-
Erro #1 — "Duplicate key name" ao criar índice.
Sintoma:ERROR 1061 (42000): Duplicate key name 'idx_nome'
Causa: Tentativa de criar um índice com nome que já existe na tabela, mesmo que em colunas diferentes.
Solução: Liste os índices existentes comSHOW INDEX FROM tabela;e escolha um nome único, ou useALTER TABLE ... ADD INDEXsem nome (o MySQL gera um automaticamente, mas isso não é recomendado para produção). Em ambientes gerenciados pela JRT Technology Solutions, padronizamos prefixos como idx_ para índices comuns e uq_ para unique indexes, evitando colisões. -
Erro #2 — Índice criado mas EXPLAIN mostra "type: ALL" (full scan).
Sintoma: O índice existe emSHOW INDEX, masEXPLAINmostrakey: NULLetype: ALL.
Causa: Geralmente coluna com baixa cardinalidade, uso de função sobre a coluna indexada (WHERE YEAR(data_venda) = 2025) — o que impede o uso do índice — ou coluna com NULL em grande proporção se for índice UNIQUE.
Solução: Remova funções do lado da coluna no WHERE. SubstituaWHERE YEAR(data) = 2025porWHERE data BETWEEN '2025-01-01' AND '2025-12-31'. Se a cardinalidade for baixa, remova o índice. UseEXPLAIN FORMAT=JSONpara ver metadados detalhados da decisão do otimizador. -
Erro #3 — Lentidão extrema em INSERT/UPDATE após criação de muitos índices.
Sintoma: Operações de escrita que antes levavam milissegundos agora demoram segundos, com picos de I/O em disco.
Causa: Cada índice adicional em uma tabela InnoDB precisa ser atualizado a cada INSERT, UPDATE que afete colunas indexadas, e DELETE. Em tabelas com 10 ou mais índices, o overhead se torna proibitivo.
Solução: Audite os índices e remova aqueles que não são utilizados. Consulte a tabelasys.schema_unused_indexes(se o sys schema estiver instalado) ou monitore comperformance_schema.table_io_waits_summary_by_index_usage. Em projetos de otimização, uma análise de workload com pt-query-digest da Percona é imprescindível. -
Erro #4 — Índice FULLTEXT não retorna resultados esperados em buscas textuais.
Sintoma:SELECT * FROM artigos WHERE MATCH(titulo, corpo) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE)retorna 0 linhas, mas você sabe que existem registros correspondentes.
Causa: O índice FULLTEXT ignora palavras muito curtas (parâmetroinnodb_ft_min_token_size, padrão 3), stopwords padrão podem estar filtrando termos, ou a coluna não foi incluída corretamente na definição do índice.
Solução: Verifique as stopwords comSELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;. Ajusteinnodb_ft_min_token_sizeno arquivomy.cnfe reconstrua o índice comALTER TABLE ... DROP INDEX ... , ADD FULLTEXT .... Teste com palavras maiores que o mínimo configurado.
Boas Práticas e Recomendações da JRT Technology Solutions
Após anos implementando e otimizando centenas de instâncias MySQL para clientes de diversos portes, consolidamos um conjunto de boas práticas que podem evitar a maioria dos problemas com índices no MySQL antes mesmo que eles surjam. A primeira e mais importante: nunca crie índices sem antes analisar as queries reais da aplicação. Use logs de slow query (slow_query_log), ferramentas como pt-query-digest e o Performance Schema para identificar exatamente quais consultas estão consumindo mais recursos. Um índice criado para uma query que roda uma vez por dia é custo puro; o mesmo índice para uma query que roda 500 vezes por segundo é investimento com retorno garantido.
Siga a regra da seletividade: as colunas mais seletivas (com maior número de valores distintos) devem vir primeiro no índice composto. Por exemplo, id_cliente (5.000 valores distintos) é mais seletivo que status (3 valores); logo, (id_cliente, status) é melhor que (status, id_cliente). Evite índices redundantes — se você tem um índice em (A, B, C), um índice
Quer aprender na prática com especialistas?
A JRT Technology Solutions oferece treinamentos e implementação de MySQL para equipes corporativas.