Aula 13: Índices no MySQL — como criar e quando usar para performance

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 com SHOW INDEX FROM tabela; e escolha um nome único, ou use ALTER TABLE ... ADD INDEX sem 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 em SHOW INDEX, mas EXPLAIN mostra key: NULL e type: 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. Substitua WHERE YEAR(data) = 2025 por WHERE data BETWEEN '2025-01-01' AND '2025-12-31'. Se a cardinalidade for baixa, remova o índice. Use EXPLAIN FORMAT=JSON para 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 tabela sys.schema_unused_indexes (se o sys schema estiver instalado) ou monitore com performance_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âmetro innodb_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 com SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;. Ajuste innodb_ft_min_token_size no arquivo my.cnf e reconstrua o índice com ALTER 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.



Falar no WhatsApp

Thiago Paes Rodrigues

Com mais de 22 anos de experiência em Tecnologia da Informação, este profissional construiu uma trajetória sólida como empresário, atuando de forma estratégica na implementação de soluções tecnológicas que otimizam processos e impulsionam resultados em diferentes setores.