{"id":1178,"date":"2026-06-19T18:12:07","date_gmt":"2026-06-19T21:12:07","guid":{"rendered":"https:\/\/jrtx.com.br\/blog\/2026\/06\/19\/aula-13-indices-no-mysql-como-criar-e-quando-usar-para-perfo\/"},"modified":"2026-06-19T18:12:07","modified_gmt":"2026-06-19T21:12:07","slug":"aula-13-indices-no-mysql-como-criar-e-quando-usar-para-perfo","status":"publish","type":"post","link":"https:\/\/jrtx.com.br\/blog\/2026\/06\/19\/aula-13-indices-no-mysql-como-criar-e-quando-usar-para-perfo\/","title":{"rendered":"Aula 13: \u00cdndices no MySQL \u2014 como criar e quando usar para performance"},"content":{"rendered":"<p>Bem-vindo \u00e0 Aula 13 do curso <strong>MySQL \u2014 Do Zero ao Avan\u00e7ado<\/strong>. Hoje, 19 de junho de 2026, vamos mergulhar em um dos pilares mais cr\u00edticos para qualquer profissional que trabalha com bancos de dados: os <strong>\u00edndices no MySQL<\/strong>. Se voc\u00ea j\u00e1 construiu tabelas e escreveu consultas SELECT, JOIN e WHERE, sabe que a diferen\u00e7a entre uma query que responde em milissegundos e outra que paralisa o servidor por minutos est\u00e1, quase sempre, na correta utiliza\u00e7\u00e3o de \u00edndices. \u00cdndices s\u00e3o estruturas auxiliares que permitem ao MySQL localizar linhas sem precisar varrer a tabela inteira \u2014 \u00e9 a diferen\u00e7a entre consultar o \u00edndice remissivo de um livro ou folhear todas as suas p\u00e1ginas uma a uma.<\/p>\n<p>Nesta aula, voc\u00ea compreender\u00e1 profundamente como os \u00edndices funcionam internamente, quais tipos o MySQL oferece, e principalmente, quando e como criar cada um deles para obter ganhos reais de performance. N\u00e3o se trata apenas de decorar a sintaxe do comando <strong>CREATE INDEX<\/strong>; precisamos entender o impacto em opera\u00e7\u00f5es de leitura e escrita, como o otimizador do MySQL decide usar ou n\u00e3o um \u00edndice e quais armadilhas podem transformar um \u00edndice bem-intencionado em um pesadelo de degrada\u00e7\u00e3o de performance. Em nossos projetos na <strong>JRT Technology Solutions<\/strong>, frequentemente somos chamados para diagnosticar sistemas lentos, e em mais de 80% dos casos, a raiz do problema est\u00e1 em \u00edndices ausentes ou mal dimensionados.<\/p>\n<p>O conte\u00fado que preparamos \u00e9 progressivo: come\u00e7aremos com a base te\u00f3rica necess\u00e1ria sobre estruturas B-Tree e Hash, passaremos para a cria\u00e7\u00e3o pr\u00e1tica de \u00edndices prim\u00e1rios, \u00fanicos, comuns, de texto completo e espaciais, e culminaremos no uso avan\u00e7ado do comando <strong>EXPLAIN<\/strong> para analisar e validar se seus \u00edndices est\u00e3o realmente sendo utilizados pelo otimizador. Cada conceito ser\u00e1 ilustrado com exemplos pr\u00e1ticos execut\u00e1veis no seu ambiente local. Voc\u00ea aprender\u00e1 tamb\u00e9m a identificar gargalos usando <strong>SHOW INDEX<\/strong>, a interpretar colunas como cardinalidade e seletividade, e a entender por que um \u00edndice perfeito no papel pode ser ignorado pelo MySQL em tempo de execu\u00e7\u00e3o.<\/p>\n<p>Esta aula tem n\u00edvel intermedi\u00e1rio porque pressup\u00f5e que voc\u00ea j\u00e1 domina a cria\u00e7\u00e3o de tabelas, chaves prim\u00e1rias e estrangeiras, e a escrita de consultas com WHERE, ORDER BY, GROUP BY e JOINs \u2014 t\u00f3picos que cobrimos em profundidade nas Aulas 6 a 12. Ao final desta aula, voc\u00ea ser\u00e1 capaz de auditar suas pr\u00f3prias tabelas, identificar colunas candidatas a indexa\u00e7\u00e3o, criar \u00edndices que realmente aceleram suas queries cr\u00edticas e evitar os principais erros que desperdi\u00e7am espa\u00e7o em disco e penalizam opera\u00e7\u00f5es de INSERT, UPDATE e DELETE. Pegue seu terminal, conecte-se ao seu servidor MySQL de testes e vamos dominar juntos o fascinante mundo dos \u00edndices.<\/p>\n<h3>O que voc\u00ea vai aprender nesta aula<\/h3>\n<ul>\n<li>Compreender o funcionamento interno de \u00edndices no MySQL \u2014 estruturas B-Tree, Hash e Full-Text<\/li>\n<li>Diferenciar os tipos de \u00edndices dispon\u00edveis: <strong>PRIMARY KEY<\/strong>, <strong>UNIQUE<\/strong>, <strong>INDEX<\/strong> (n\u00e3o \u00fanico), <strong>FULLTEXT<\/strong> e <strong>SPATIAL<\/strong><\/li>\n<li>Criar, alterar e remover \u00edndices utilizando <strong>CREATE INDEX<\/strong>, <strong>ALTER TABLE &#8230; ADD INDEX<\/strong> e <strong>DROP INDEX<\/strong><\/li>\n<li>Projetar \u00edndices compostos (multicoluna) seguindo a regra da coluna mais seletiva primeiro<\/li>\n<li>Utilizar o comando <strong>EXPLAIN<\/strong> para verificar se um \u00edndice est\u00e1 sendo usado e interpretar o plano de execu\u00e7\u00e3o<\/li>\n<li>Monitorar e auditar \u00edndices existentes com <strong>SHOW INDEX<\/strong> e <strong>INFORMATION_SCHEMA<\/strong><\/li>\n<li>Identificar e corrigir erros comuns que tornam \u00edndices ineficazes ou prejudiciais<\/li>\n<\/ul>\n<h3>Pr\u00e9-requisitos e Ambiente<\/h3>\n<p>Para acompanhar esta aula com m\u00e1ximo proveito, voc\u00ea precisar\u00e1 de um servidor <strong>MySQL 8.0<\/strong> ou superior em funcionamento. Assumimos que voc\u00ea j\u00e1 possui o MySQL instalado e configurado (Aulas 2 e 3), sabe criar bancos de dados e tabelas (Aulas 6 e 7), e est\u00e1 confort\u00e1vel escrevendo consultas com WHERE, JOIN e ORDER BY (Aulas 10, 11 e 12). Utilizaremos o banco de dados de demonstra\u00e7\u00e3o <strong>curso_mysql<\/strong> que constru\u00edmos ao longo do curso. Recomendamos que voc\u00ea execute todos os comandos em um ambiente de testes, pois faremos inser\u00e7\u00f5es em massa para demonstrar o impacto de \u00edndices na pr\u00e1tica. Teremos dois terminais abertos: um com o cliente <strong>mysql<\/strong> conectado e outro para comandos do sistema operacional, se necess\u00e1rio. Todos os exemplos foram testados em Ubuntu 24.04 LTS e Rocky Linux 9, com MySQL 8.0.37 \u2014 as sa\u00eddas podem variar ligeiramente em vers\u00f5es diferentes, mas os conceitos permanecem id\u00eanticos.<\/p>\n<h3>Entendendo \u00cdndices no MySQL \u2014 Estrutura Interna e Tipos<\/h3>\n<p>Quando falamos em <strong>\u00edndices no MySQL<\/strong>, estamos nos referindo a estruturas de dados auxiliares que o motor de armazenamento (normalmente o InnoDB) mant\u00e9m para acelerar a localiza\u00e7\u00e3o de registros. Imagine uma tabela <strong>clientes<\/strong> com 2 milh\u00f5es de linhas e uma consulta como <code>SELECT * FROM clientes WHERE cpf = '123.456.789-00'<\/code>. Sem \u00edndice, o MySQL precisa ler cada uma das 2 milh\u00f5es de linhas sequencialmente \u2014 \u00e9 o temido <em>full table scan<\/em>. Com um \u00edndice na coluna <strong>cpf<\/strong>, o MySQL pode navegar por uma estrutura de \u00e1rvore balanceada (B-Tree) e localizar o registro em 3 ou 4 saltos, uma diferen\u00e7a brutal de desempenho.<\/p>\n<p>Por padr\u00e3o, o InnoDB organiza fisicamente os dados em uma estrutura chamada <strong>\u00edndice clusterizado<\/strong> (clustered index), que \u00e9 constru\u00eddo sobre a chave prim\u00e1ria da tabela. Isso significa que as linhas da tabela s\u00e3o armazenadas nas folhas da B-Tree da PRIMARY KEY \u2014 entenda isso como se o conte\u00fado da tabela e o \u00edndice prim\u00e1rio fossem a mesma estrutura. Todos os demais \u00edndices s\u00e3o chamados de <strong>\u00edndices secund\u00e1rios<\/strong> (secondary indexes) e armazenam em suas folhas uma c\u00f3pia do valor da chave prim\u00e1ria correspondente, e n\u00e3o o endere\u00e7o f\u00edsico da linha. Essa compreens\u00e3o \u00e9 vital: ao buscar por um \u00edndice secund\u00e1rio, o MySQL primeiro encontra o \u00edndice, obt\u00e9m a PRIMARY KEY associada e depois acessa o \u00edndice clusterizado \u2014 processo conhecido como <em>lookup<\/em> duplo.<\/p>\n<p>Os <strong>\u00edndices no MySQL<\/strong> podem ser de v\u00e1rios tipos conforme sua finalidade. O <strong>PRIMARY KEY<\/strong> \u00e9 o \u00edndice prim\u00e1rio e clusterizado (no InnoDB), que garante unicidade e n\u00e3o permite NULLs. O <strong>UNIQUE INDEX<\/strong> tamb\u00e9m garante unicidade, mas permite NULLs e cria um \u00edndice secund\u00e1rio. O <strong>INDEX<\/strong> simples (ou n\u00e3o \u00fanico) acelera consultas sem impor restri\u00e7\u00f5es de unicidade. \u00cdndices <strong>FULLTEXT<\/strong> s\u00e3o especializados para buscas em campos de texto longo com linguagem natural e modo booleano. J\u00e1 os \u00edndices <strong>SPATIAL<\/strong> (ou R-Tree) otimizam consultas sobre dados geoespaciais \u2014 este \u00faltimo dispon\u00edvel apenas para tabelas MyISAM em vers\u00f5es antigas, mas no MySQL 8.0 o InnoDB tamb\u00e9m oferece suporte a \u00edndices espaciais. Na pr\u00e1tica di\u00e1ria, voc\u00ea trabalhar\u00e1 95% do tempo com PRIMARY KEY, UNIQUE e INDEX simples, mas \u00e9 importante conhecer todo o arsenal.<\/p>\n<p>Internamente, a B-Tree do InnoDB \u00e9 uma \u00e1rvore balanceada onde cada p\u00e1gina (geralmente 16KB) cont\u00e9m m\u00faltiplas chaves e ponteiros. Isso minimiza o n\u00famero de opera\u00e7\u00f5es de leitura em disco, mantendo a profundidade da \u00e1rvore baixa. Para tabelas com centenas de milh\u00f5es de registros, uma B-Tree t\u00edpica tem 4 ou 5 n\u00edveis de profundidade. J\u00e1 os \u00edndices Hash estariam dispon\u00edveis teoricamente no motor MEMORY, mas seu uso moderno \u00e9 raro \u2014 focaremos na B-Tree, que \u00e9 a espinha dorsal de praticamente tudo no InnoDB. Com essa base te\u00f3rica clara, podemos passar \u00e0 cria\u00e7\u00e3o pr\u00e1tica, onde a teoria se transforma em ganhos mensur\u00e1veis de performance.<\/p>\n<h3>Criando \u00cdndices no MySQL na Pr\u00e1tica \u2014 Sintaxe Completa e Exemplos Reais<\/h3>\n<p>Vamos construir um cen\u00e1rio realista de laborat\u00f3rio. 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\u00e7a dos \u00edndices. Vamos gerar 500 mil registros utilizando um procedimento armazenado simples \u2014 essa massa de dados tornar\u00e1 os tempos de resposta facilmente percept\u00edveis mesmo em m\u00e1quinas modestas.<\/p>\n<p><strong>Passo 1:<\/strong> Acesse o MySQL e crie o banco de dados da aula.<\/p>\n<pre><code>-- Conecte-se ao MySQL como root ou usu\u00e1rio com privil\u00e9gios CREATE DATABASE\nmysql -u root -p\n\n\/* ============================================================\n   Aula 13 \u2014 Criando base de testes para \u00cdndices no MySQL\n   ============================================================ *\/\n\n-- Criando o banco de dados dedicado para a aula\nCREATE DATABASE IF NOT EXISTS aula13_indices\n    CHARACTER SET utf8mb4\n    COLLATE utf8mb4_unicode_ci;\n\n-- Selecionando o banco rec\u00e9m-criado\nUSE aula13_indices;\n\n-- Criando tabela de vendas SEM \u00edndices adicionais (apenas a PK)\nCREATE TABLE vendas (\n    id BIGINT AUTO_INCREMENT,\n    data_venda DATE NOT NULL,\n    id_cliente INT NOT NULL,\n    id_produto INT NOT NULL,\n    quantidade INT NOT NULL DEFAULT 1,\n    valor_unitario DECIMAL(10, 2) NOT NULL,\n    valor_total DECIMAL(12, 2) GENERATED ALWAYS AS (quantidade * valor_unitario) STORED,\n    status ENUM('pendente', 'aprovada', 'cancelada') NOT NULL DEFAULT 'pendente',\n    observacao TEXT,\n    PRIMARY KEY (id)\n) ENGINE=InnoDB;\n<\/code><\/pre>\n<p>Acabamos de criar uma tabela <strong>vendas<\/strong> com apenas a chave prim\u00e1ria como \u00edndice. As colunas <strong>id_cliente<\/strong>, <strong>id_produto<\/strong>, <strong>data_venda<\/strong> e <strong>status<\/strong> \u2014 que certamente ser\u00e3o usadas em filtros \u2014 est\u00e3o completamente desindexadas neste momento. Essa \u00e9 exatamente a situa\u00e7\u00e3o que encontramos em sistemas reais que cresceram sem planejamento de \u00edndices.<\/p>\n<p><strong>Passo 2:<\/strong> Vamos popular a tabela com 500 mil registros usando um procedimento que insere dados pseudoaleat\u00f3rios.<\/p>\n<pre><code>-- Alterando delimitador para criar procedure\nDELIMITER \/\/\n\nCREATE PROCEDURE popular_vendas(IN total_registros INT)\nBEGIN\n    DECLARE i INT DEFAULT 0;\n    DECLARE data_base DATE DEFAULT '2024-01-01';\n    WHILE i < total_registros DO\n        INSERT INTO vendas (data_venda, id_cliente, id_produto, quantidade, valor_unitario, status, observacao)\n        VALUES (\n            DATE_ADD(data_base, INTERVAL FLOOR(RAND() * 730) DAY),\n            FLOOR(1 + RAND() * 5000),\n            FLOOR(1 + RAND() * 800),\n            FLOOR(1 + RAND() * 20),\n            ROUND(10 + RAND() * 990, 2),\n            ELT(FLOOR(1 + RAND() * 3), 'pendente', 'aprovada', 'cancelada'),\n            CONCAT('Venda registrada automaticamente - ciclo ', i)\n        );\n        SET i = i + 1;\n        -- Commit a cada 10.000 linhas para n\u00e3o estourar buffer\n        IF i % 10000 = 0 THEN\n            COMMIT;\n        END IF;\n    END WHILE;\n    COMMIT;\nEND \/\/\n\nDELIMITER ;\n\n-- Executando o procedimento para gerar 500.000 registros\nCALL popular_vendas(500000);\n\n-- Verificando quantos registros foram inseridos\nSELECT COUNT(*) AS total_registros FROM vendas;\n<\/code><\/pre>\n<pre><code class=\"output\">+-----------------+\n| total_registros |\n+-----------------+\n|          500000 |\n+-----------------+\n1 row in set (0.01 sec)\n<\/code><\/pre>\n<p>Temos agora meio milh\u00e3o de vendas na tabela. Vamos testar a performance de uma consulta t\u00edpica que um sistema de ERP ou e-commerce faria: buscar vendas de um cliente espec\u00edfico em um determinado per\u00edodo. Ative o perfil de tempo de execu\u00e7\u00e3o para medir exatamente o impacto.<\/p>\n<pre><code>-- Habilitando profiling para medir tempo de execu\u00e7\u00e3o (MySQL 8.0)\nSET profiling = 1;\n\n-- Consulta sem \u00edndice: buscar vendas do cliente 725 no primeiro trimestre de 2025\nSELECT id, data_venda, id_produto, quantidade, valor_total, status\nFROM vendas\nWHERE id_cliente = 725\n  AND data_venda BETWEEN '2025-01-01' AND '2025-03-31'\nORDER BY data_venda DESC;\n\n-- Exibindo profile da consulta\nSHOW PROFILES;\n<\/code><\/pre>\n<pre><code class=\"output\">+----------+------------+----------------------------------------------------------------------------------------------+\n| Query_ID | Duration   | Query                                                                                        |\n+----------+------------+----------------------------------------------------------------------------------------------+\n|        1 | 0.45280750 | SELECT id, data_venda, ... FROM vendas WHERE id_cliente = 725 AND data_venda BETWEEN ...     |\n+----------+------------+----------------------------------------------------------------------------------------------+\n1 row in set (0.00 sec)\n<\/code><\/pre>\n<p><strong>0,45 segundos<\/strong> \u2014 em meio milh\u00e3o de linhas. Pode parecer aceit\u00e1vel, mas quando essa consulta \u00e9 executada centenas de vezes por minuto sob carga concorrente, o impacto \u00e9 devastador. E lembre-se: 500 mil linhas \u00e9 uma tabela pequena para padr\u00f5es de produ\u00e7\u00e3o. Agora, vamos criar nosso primeiro \u00edndice direcionado e medir a diferen\u00e7a.<\/p>\n<p><strong>Passo 3:<\/strong> Criando um \u00edndice composto nas colunas <strong>id_cliente<\/strong> e <strong>data_venda<\/strong>.<\/p>\n<pre><code>-- Criando \u00edndice composto: id_cliente + data_venda\n-- A ordem importa: coluna de igualdade primeiro, coluna de range depois\nCREATE INDEX idx_vendas_cliente_data ON vendas (id_cliente, data_venda);\n\n-- Verificando os \u00edndices da tabela\nSHOW INDEX FROM vendas;\n<\/code><\/pre>\n<pre><code class=\"output\">+--------+------------+---------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+\n| Table  | Non_unique | Key_name                  | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |\n+--------+------------+---------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+\n| vendas |          0 | PRIMARY                   |            1 | id          | A         |      498200 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |\n| vendas |          1 | idx_vendas_cliente_data   |            1 | id_cliente  | A         |        4842 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |\n| vendas |          1 | idx_vendas_cliente_data   |            2 | data_venda  | A         |      481693 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |\n+--------+------------+---------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+\n3 rows in set (0.00 sec)\n<\/code><\/pre>\n<p>A sa\u00edda do <strong>SHOW INDEX<\/strong> nos revela informa\u00e7\u00f5es preciosas. O \u00edndice <strong>idx_vendas_cliente_data<\/strong> \u00e9 do tipo <strong>BTREE<\/strong>, possui duas colunas na sequ\u00eancia que definimos, e o campo <strong>Cardinality<\/strong> (cardinalidade) nos d\u00e1 uma estimativa de quantos valores distintos existem em cada n\u00edvel do \u00edndice. Observe que <strong>id_cliente<\/strong> tem cardinalidade aproximada de 4.842 (coerente com 5.000 clientes poss\u00edveis no nosso script), e a combina\u00e7\u00e3o com <strong>data_venda<\/strong> salta para quase 482 mil \u2014 ou seja, a combina\u00e7\u00e3o das duas colunas \u00e9 altamente seletiva, exatamente o que buscamos. Vamos agora repetir a mesma consulta e conferir o ganho.<\/p>\n<pre><code>-- Repetindo a consulta anterior (agora com \u00edndice)\nSELECT id, data_venda, id_produto, quantidade, valor_total, status\nFROM vendas\nWHERE id_cliente = 725\n  AND data_venda BETWEEN '2025-01-01' AND '2025-03-31'\nORDER BY data_venda DESC;\n\nSHOW PROFILES;\n<\/code><\/pre>\n<pre><code class=\"output\">+----------+------------+----------------------------------------------------------------------------------------------+\n| Query_ID | Duration   | Query                                                                                        |\n+----------+------------+----------------------------------------------------------------------------------------------+\n|        1 | 0.45280750 | ... (execu\u00e7\u00e3o sem \u00edndice)                                                                    |\n|        2 | 0.00112500 | SELECT id, data_venda, ... FROM vendas WHERE id_cliente = 725 AND data_venda BETWEEN ...     |\n+----------+------------+----------------------------------------------------------------------------------------------+\n2 rows in set (0.00 sec)\n<\/code><\/pre>\n<p>De <strong>0,45 segundos<\/strong> para <strong>0,0011 segundos<\/strong> \u2014 um ganho de aproximadamente <strong>400 vezes<\/strong>! Isso \u00e9 o poder de um \u00edndice bem projetado. O MySQL n\u00e3o precisou mais varrer a tabela inteira; ele navegou diretamente pela B-Tree, localizou o n\u00f3 correspondente ao cliente 725 e percorreu sequencialmente as entradas dentro do range de datas especificado. Em nossos projetos na <strong>JRT Technology Solutions<\/strong>, \u00e9 exatamente esse tipo de otimiza\u00e7\u00e3o que transforma sistemas lentos e reativos em plataformas escal\u00e1veis que suportam milhares de transa\u00e7\u00f5es por segundo.<\/p>\n<h3>\u00cdndices Compostos e Cobertura \u2014 Maximizando a Performance<\/h3>\n<p>Um dos maiores equ\u00edvocos que encontramos em equipes de desenvolvimento \u00e9 a cria\u00e7\u00e3o de m\u00faltiplos \u00edndices simples em cada coluna usada em WHERE, em vez de projetar \u00edndices compostos estrat\u00e9gicos. <strong>\u00cdndices no MySQL<\/strong> compostos (ou multicoluna) seguem o princ\u00edpio do prefixo mais \u00e0 esquerda: o MySQL pode utilizar o \u00edndice se a consulta filtrar pela primeira coluna, pelas duas primeiras, pelas tr\u00eas primeiras, e assim por diante \u2014 mas nunca \"pulando\" colunas. Nosso \u00edndice <strong>idx_vendas_cliente_data (id_cliente, data_venda)<\/strong> ser\u00e1 utilizado em uma consulta que filtra apenas por <strong>id_cliente<\/strong>, mas ser\u00e1 ignorado se filtrarmos apenas por <strong>data_venda<\/strong> (a menos que seja um <em>index skip scan<\/em>, recurso do MySQL 8.0 que discutiremos a seguir).<\/p>\n<p>Uma t\u00e9cnica avan\u00e7ada e extremamente eficiente \u00e9 o <strong>\u00edndice de cobertura<\/strong> (covering index). Ele ocorre quando todas as colunas que a consulta precisa \u2014 tanto no SELECT quanto no WHERE, JOIN, ORDER BY e GROUP BY \u2014 est\u00e3o presentes no \u00edndice. Com isso, o MySQL pode satisfazer toda a consulta lendo apenas o \u00edndice secund\u00e1rio, sem nunca precisar acessar o \u00edndice clusterizado para buscar a linha completa. Esse \u00e9 o \"Santo Graal\" da otimiza\u00e7\u00e3o: opera\u00e7\u00f5es de leitura puramente sequenciais dentro da B-Tree do \u00edndice, sem saltos aleat\u00f3rios para a tabela. Vamos criar um \u00edndice de cobertura para uma consulta frequente no nosso sistema fict\u00edcio: o dashboard de vendas por produto e status.<\/p>\n<pre><code>-- Suponha que a query cr\u00edtica do dashboard seja:\n-- SELECT id_produto, status, COUNT(*), SUM(valor_total)\n-- FROM vendas\n-- WHERE data_venda >= '2025-06-01'\n-- GROUP BY id_produto, status;\n\n-- Criando um \u00edndice que cubra: data_venda (WHERE), id_produto e status (GROUP BY) e valor_total (agrega\u00e7\u00e3o)\nCREATE INDEX idx_vendas_dashboard ON vendas (data_venda, id_produto, status, valor_total);\n\n-- Executando a consulta com EXPLAIN para verificar cobertura\nEXPLAIN SELECT id_produto, status, COUNT(*), SUM(valor_total)\nFROM vendas\nWHERE data_venda >= '2025-06-01'\nGROUP BY id_produto, status;\n<\/code><\/pre>\n<pre><code class=\"output\">+----+-------------+--------+------------+-------+------------------------------------------+-----------------------+---------+------+--------+----------+--------------------------+\n| id | select_type | table  | partitions | type  | possible_keys                            | key                   | key_len | ref  | rows   | filtered | Extra                    |\n+----+-------------+--------+------------+-------+------------------------------------------+-----------------------+---------+------+--------+----------+--------------------------+\n|  1 | SIMPLE      | vendas | NULL       | range | idx_vendas_dashboard,idx_vendas_cliente_data | idx_vendas_dashboard | 3       | NULL | 124998 |   100.00 | Using where; Using index |\n+----+-------------+--------+------------+-------+------------------------------------------+-----------------------+---------+------+--------+----------+--------------------------+\n1 row in set (0.00 sec)\n<\/code><\/pre>\n<p>Observe o valor <strong>\"Using index\"<\/strong> na coluna <strong>Extra<\/strong>. Isso \u00e9 a assinatura do \u00edndice de cobertura: o MySQL est\u00e1 satisfazendo a consulta inteiramente a partir do \u00edndice, sem acessar os dados da tabela. A coluna <strong>type<\/strong> mostra <strong>range<\/strong>, indicando que o MySQL est\u00e1 percorrendo um intervalo da B-Tree (devido ao <code>data_venda >= '2025-06-01'<\/code>). A coluna <strong>rows<\/strong> estima ~125 mil linhas examinadas; com o \u00edndice correto, mesmo esse volume \u00e9 processado de forma extremamente eficiente.<\/p>\n<p>Projetar \u00edndices de cobertura demanda que voc\u00ea conhe\u00e7a profundamente as queries que sua aplica\u00e7\u00e3o executa. A ordem das colunas no \u00edndice deve ser: primeiro colunas usadas em condi\u00e7\u00f5es de igualdade no WHERE, depois colunas de range, e ent\u00e3o colunas do GROUP BY e ORDER BY. Colunas do SELECT podem vir por \u00faltimo, desde que isso n\u00e3o prejudique a seletividade do prefixo. Um erro comum \u00e9 tentar cobrir todas as consultas com um \u00fanico \u00edndice gigante \u2014 isso desperdi\u00e7a espa\u00e7o e penaliza escritas. Encontre um equil\u00edbrio baseado no perfil de uso real da sua aplica\u00e7\u00e3o, algo que nossos especialistas da JRT Technology Solutions fazem utilizando ferramentas como o <strong>sys schema<\/strong> e <strong>Performance Schema<\/strong> do MySQL para identificar as queries mais custosas.<\/p>\n<h3>Analisando o Uso de \u00cdndices com EXPLAIN \u2014 O Raio-X das Suas Queries<\/h3>\n<p>Nenhuma discuss\u00e3o sobre <strong>\u00edndices no MySQL<\/strong> est\u00e1 completa sem dominar o comando <strong>EXPLAIN<\/strong>. Ele \u00e9 o seu principal aliado para entender o que o otimizador est\u00e1 realmente fazendo com sua consulta. Vamos analisar em detalhes a sa\u00edda do EXPLAIN, coluna por coluna, para que voc\u00ea possa auditar qualquer query e decidir com seguran\u00e7a se os \u00edndices est\u00e3o cumprindo seu prop\u00f3sito.<\/p>\n<table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse: collapse; width: 100%; margin-bottom: 20px;\">\n<thead>\n<tr style=\"background-color: #f0f0f0;\">\n<th style=\"text-align: left;\">Coluna<\/th>\n<th style=\"text-align: left;\">Valores Comuns<\/th>\n<th style=\"text-align: left;\">Interpreta\u00e7\u00e3o e Impacto<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>id<\/strong><\/td>\n<td>1, 2, NULL<\/td>\n<td>Identificador da etapa no plano. Queries com subconsultas ou UNION geram m\u00faltiplos ids. Etapas com mesmo id s\u00e3o executadas em paralelo.<\/td>\n<\/tr>\n<tr>\n<td><strong>select_type<\/strong><\/td>\n<td>SIMPLE, PRIMARY, SUBQUERY, DERIVED, UNION<\/td>\n<td>Tipo de opera\u00e7\u00e3o da linha. SIMPLE \u00e9 uma consulta direta sem subselects. DERIVED indica subquery no FROM (tabela tempor\u00e1ria).<\/td>\n<\/tr>\n<tr>\n<td><strong>table<\/strong><\/td>\n<td>Nome da tabela ou alias<\/td>\n<td>Indica a tabela acessada. Pode ser &lt;derivedN&gt; para tabelas tempor\u00e1rias geradas por subqueries.<\/td>\n<\/tr>\n<tr>\n<td><strong>type<\/strong><\/td>\n<td>ALL, index, range, ref, eq_ref, const, system<\/td>\n<td><strong>O mais cr\u00edtico:<\/strong> m\u00e9todo de acesso. ALL = full table scan (RUIM). index = scan do \u00edndice (nem sempre bom). range = varredura de intervalo (aceit\u00e1vel). ref = busca por \u00edndice n\u00e3o \u00fanico (BOM). eq_ref = busca por \u00edndice \u00fanico em JOIN (\u00d3TIMO). const = busca por PRIMARY KEY ou UNIQUE com valor constante (EXCELENTE).<\/td>\n<\/tr>\n<tr>\n<td><strong>possible_keys<\/strong><\/td>\n<td>Lista de \u00edndices candidatos<\/td>\n<td>\u00cdndices que o otimizador considerou usar. Se vazio, n\u00e3o h\u00e1 \u00edndice dispon\u00edvel para o filtro \u2014 grave sinal de alerta.<\/td>\n<\/tr>\n<tr>\n<td><strong>key<\/strong><\/td>\n<td>Nome do \u00edndice escolhido<\/td>\n<td>\u00cdndice efetivamente usado. Se NULL, o otimizador optou por n\u00e3o usar \u00edndice (poss\u00edvel otimizador for\u00e7ando table scan).<\/td>\n<\/tr>\n<tr>\n<td><strong>key_len<\/strong><\/td>\n<td>Tamanho em bytes da chave usada<\/td>\n<td>Permite inferir quantas colunas do \u00edndice composto est\u00e3o sendo efetivamente utilizadas. Ex: INT = 4 bytes, DATE = 3 bytes.<\/td>\n<\/tr>\n<tr>\n<td><strong>rows<\/strong><\/td>\n<td>N\u00famero estimado de linhas examinadas<\/td>\n<td>Estimativa do otimizador. Valores altos em tabelas grandes indicam poss\u00edvel falta de \u00edndice seletivo.<\/td>\n<\/tr>\n<tr>\n<td><strong>Extra<\/strong><\/td>\n<td>Using index, Using where, Using temporary, Using filesort<\/td>\n<td><strong>\"Using index\"<\/strong> = \u00edndice de cobertura (desej\u00e1vel). <strong>\"Using temporary\"<\/strong> = tabela tempor\u00e1ria criada (custo alto, repense GROUP BY \/ ORDER BY). <strong>\"Using filesort\"<\/strong> = ordena\u00e7\u00e3o em disco (evite com \u00edndice ordenado corretamente).<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Vamos testar cen\u00e1rios problem\u00e1ticos para visualizar os sinais de alerta no EXPLAIN. Primeiro, uma consulta que for\u00e7a <strong>filesort<\/strong> \u2014 ou seja, o MySQL precisa ordenar resultados manualmente porque o \u00edndice n\u00e3o cobre a cl\u00e1usula ORDER BY.<\/p>\n<pre><code>-- Consulta com filesort: ordena\u00e7\u00e3o por coluna que n\u00e3o est\u00e1 no \u00edndice ap\u00f3s o range\nEXPLAIN SELECT id_cliente, data_venda, valor_total\nFROM vendas\nWHERE id_cliente = 725\nORDER BY valor_total DESC;\n<\/code><\/pre>\n<pre><code class=\"output\">+----+-------------+--------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------------+\n| id | select_type | table  | type | possible_keys           | key                     | key_len | ref   | rows | filtered | Extra                       |\n+----+-------------+--------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------------+\n|  1 | SIMPLE      | vendas | ref  | idx_vendas_cliente_data | idx_vendas_cliente_data | 4       | const |  103 |   100.00 | Using where; Using filesort |\n+----+-------------+--------+------+-------------------------+-------------------------+---------+-------+------+----------+-----------------------------+\n<\/code><\/pre>\n<p>Observe <strong>\"Using filesort\"<\/strong> no campo Extra. Embora o \u00edndice <strong>idx_vendas_cliente_data<\/strong> esteja sendo usado para localizar o cliente (type=ref, um bom sinal), a ordena\u00e7\u00e3o por <strong>valor_total<\/strong> \u2014 coluna que n\u00e3o faz parte do \u00edndice \u2014 for\u00e7a uma opera\u00e7\u00e3o de ordena\u00e7\u00e3o \u00e0 parte. Para evitar isso, precisar\u00edamos de um \u00edndice que inclua <strong>valor_total<\/strong> na posi\u00e7\u00e3o correta ou repensar a necessidade dessa ordena\u00e7\u00e3o espec\u00edfica.<\/p>\n<p>Outro cen\u00e1rio cl\u00e1ssico \u00e9 a cria\u00e7\u00e3o de um \u00edndice que o otimizador simplesmente ignora porque ele n\u00e3o o considera vantajoso \u2014 geralmente quando a cardinalidade \u00e9 muito baixa. Vamos criar um \u00edndice na coluna <strong>status<\/strong>, que tem apenas 3 valores distintos, e ver o que acontece.<\/p>\n<pre><code>-- Criando \u00edndice em coluna de baixa cardinalidade (armadilha comum)\nCREATE INDEX idx_vendas_status ON vendas (status);\n\n-- Testando com EXPLAIN\nEXPLAIN SELECT * FROM vendas WHERE status = 'aprovada';\n<\/code><\/pre>\n<pre><code class=\"output\">+----+-------------+--------+------+-------------------+------+---------+------+--------+----------+-------------+\n| id | select_type | table  | type | possible_keys     | key  | key_len | ref  | rows   | filtered | Extra       |\n+----+-------------+--------+------+-------------------+------+---------+------+--------+----------+-------------+\n|  1 | SIMPLE      | vendas | ALL  | idx_vendas_status | NULL | NULL    | NULL | 498200 |    33.33 | Using where |\n+----+-------------+--------+------+-------------------+------+---------+------+--------+----------+-------------+\n<\/code><\/pre>\n<p>Mesmo existindo o \u00edndice <strong>idx_vendas_status<\/strong>, o EXPLAIN mostra <strong>type=ALL<\/strong> e <strong>key=NULL<\/strong> \u2014 o otimizador preferiu fazer um full table scan. Por qu\u00ea? Porque a coluna <strong>status<\/strong> tem apenas 3 valores, gerando baix\u00edssima seletividade (aproximadamente 33% das linhas para cada valor). Nesses casos, varrer a tabela inteira sequencialmente \u00e9 frequentemente mais r\u00e1pido do que navegar pela B-Tree e depois pular para as p\u00e1ginas de dados. Essa \u00e9 uma li\u00e7\u00e3o valiosa: <strong>\u00edndices em colunas de baixa cardinalidade raramente s\u00e3o \u00fateis<\/strong> e ainda custam espa\u00e7o e performance de escrita. Remova esse \u00edndice imediatamente:<\/p>\n<pre><code>-- Removendo \u00edndice ineficaz\nDROP INDEX idx_vendas_status ON vendas;\n<\/code><\/pre>\n<h3>Gerenciando e Monitorando \u00cdndices no MySQL \u2014 Comandos Essenciais<\/h3>\n<p>Administrar <strong>\u00edndices no MySQL<\/strong> vai muito al\u00e9m de cri\u00e1-los e esquec\u00ea-los. \u00c9 necess\u00e1rio monitorar seu uso, tamanho e impacto ao longo do tempo. O comando <strong>SHOW INDEX<\/strong> que j\u00e1 utilizamos \u00e9 a porta de entrada, mas voc\u00ea pode extrair informa\u00e7\u00f5es mais ricas consultando a <strong>INFORMATION_SCHEMA<\/strong> e tabelas do <strong>Performance Schema<\/strong>. A tabela a seguir resume os principais comandos administrativos e suas finalidades:<\/p>\n<table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse: collapse; width: 100%; margin-bottom: 20px;\">\n<thead>\n<tr style=\"background-color: #f0f0f0;\">\n<th style=\"text-align: left;\">Comando \/ Consulta<\/th>\n<th style=\"text-align: left;\">Finalidade<\/th>\n<th style=\"text-align: left;\">Exemplo de Uso<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>SHOW INDEX FROM tabela<\/strong><\/td>\n<td>Lista todos os \u00edndices de uma tabela com cardinalidade, tipo, colunas e visibilidade<\/td>\n<td><code>SHOW INDEX FROM vendas;<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>CREATE INDEX nome ON tabela (colunas)<\/strong><\/td>\n<td>Cria \u00edndice simples ou composto. Aceita op\u00e7\u00f5es como USING BTREE\/HASH, COMMENT, VISIBLE\/INVISIBLE<\/td>\n<td><code>CREATE INDEX idx_data ON vendas(data_venda) COMMENT '\u00cdndice para relat\u00f3rios di\u00e1rios';<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>CREATE UNIQUE INDEX nome ON tabela (colunas)<\/strong><\/td>\n<td>Cria \u00edndice \u00fanico, garantindo n\u00e3o haver valores duplicados nas colunas especificadas<\/td>\n<td><code>CREATE UNIQUE INDEX uq_email ON usuarios(email);<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>ALTER TABLE tabela ADD INDEX nome (colunas)<\/strong><\/td>\n<td>Alternativa ao CREATE INDEX; mesma funcionalidade com sintaxe de altera\u00e7\u00e3o de schema<\/td>\n<td><code>ALTER TABLE vendas ADD INDEX idx_valor (valor_total);<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>DROP INDEX nome ON tabela<\/strong><\/td>\n<td>Remove um \u00edndice. Libera espa\u00e7o em disco e elimina overhead em escritas<\/td>\n<td><code>DROP INDEX idx_data ON vendas;<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>ALTER TABLE tabela ALTER INDEX nome VISIBLE\/INVISIBLE<\/strong><\/td>\n<td>Torna o \u00edndice invis\u00edvel para o otimizador sem remov\u00ea-lo \u2014 \u00fatil para testar impacto antes de dropar definitivamente<\/td>\n<td><code>ALTER TABLE vendas ALTER INDEX idx_data INVISIBLE;<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>SELECT * FROM information_schema.STATISTICS WHERE TABLE_NAME='vendas';<\/strong><\/td>\n<td>Vers\u00e3o SQL do SHOW INDEX, permite filtrar e fazer joins com outras tabelas do INFORMATION_SCHEMA<\/td>\n<td>Obter cardinalidade e tamanho estimado de todos os \u00edndices do banco<\/td>\n<\/tr>\n<tr>\n<td><strong>ANALYZE TABLE tabela;<\/strong><\/td>\n<td>Atualiza as estat\u00edsticas de distribui\u00e7\u00e3o de chaves (cardinalidade), melhorando as decis\u00f5es do otimizador<\/td>\n<td><code>ANALYZE TABLE vendas;<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>OPTIMIZE TABLE tabela;<\/strong><\/td>\n<td>Reorganiza fisicamente a tabela e \u00edndices, recuperando espa\u00e7o fragmentado. Trava a tabela \u2014 use com cautela<\/td>\n<td><code>OPTIMIZE TABLE vendas;<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Uma funcionalidade pouco explorada mas extremamente \u00fatil introduzida no MySQL 8.0 s\u00e3o os <strong>\u00edndices invis\u00edveis<\/strong>. Antes de remover um \u00edndice que voc\u00ea suspeita n\u00e3o estar sendo utilizado, voc\u00ea pode torn\u00e1-lo invis\u00edvel para o otimizador e observar o comportamento da aplica\u00e7\u00e3o durante alguns dias. Se nenhuma degrada\u00e7\u00e3o for percebida, voc\u00ea pode dropar o \u00edndice com seguran\u00e7a. \u00c9 uma pr\u00e1tica que recomendamos fortemente em nossos clientes na <strong>JRT Technology Solutions<\/strong> durante projetos de otimiza\u00e7\u00e3o de bases legadas.<\/p>\n<pre><code>-- Tornando um \u00edndice invis\u00edvel para teste\nALTER TABLE vendas ALTER INDEX idx_vendas_dashboard INVISIBLE;\n\n-- Verificando visibilidade dos \u00edndices\nSELECT INDEX_NAME, IS_VISIBLE\nFROM INFORMATION_SCHEMA.STATISTICS\nWHERE TABLE_SCHEMA = 'aula13_indices'\n  AND TABLE_NAME = 'vendas'\n  AND INDEX_NAME = 'idx_vendas_dashboard';\n\n-- Se a performance n\u00e3o for afetada, remova-o definitivamente\n-- DROP INDEX idx_vendas_dashboard ON vendas;\n-- Caso contr\u00e1rio, torne-o vis\u00edvel novamente:\n-- ALTER TABLE vendas ALTER INDEX idx_vendas_dashboard VISIBLE;\n<\/code><\/pre>\n<h3>Verificando os \u00cdndices e Testando a Configura\u00e7\u00e3o<\/h3>\n<p>Esta se\u00e7\u00e3o obrigat\u00f3ria garante que todos os conceitos aplicados at\u00e9 aqui est\u00e3o funcionando conforme esperado. Execute a sequ\u00eancia de verifica\u00e7\u00f5es abaixo no seu ambiente e confira se as sa\u00eddas s\u00e3o compat\u00edveis. O objetivo \u00e9 criar uma auditoria completa dos \u00edndices da nossa tabela de testes, medir o espa\u00e7o ocupado e validar que as queries est\u00e3o utilizando os \u00edndices corretamente.<\/p>\n<pre><code>-- 1. Listar todos os \u00edndices da tabela vendas com detalhes de cardinalidade e tipo\nSELECT\n    INDEX_NAME,\n    COLUMN_NAME,\n    SEQ_IN_INDEX,\n    INDEX_TYPE,\n    CARDINALITY,\n    NULLABLE,\n    IS_VISIBLE\nFROM INFORMATION_SCHEMA.STATISTICS\nWHERE TABLE_SCHEMA = 'aula13_indices'\n  AND TABLE_NAME = 'vendas'\nORDER BY INDEX_NAME, SEQ_IN_INDEX;\n<\/code><\/pre>\n<pre><code class=\"output\">+---------------------------+-------------+--------------+------------+-------------+----------+------------+\n| INDEX_NAME                | COLUMN_NAME | SEQ_IN_INDEX | INDEX_TYPE | CARDINALITY | NULLABLE | IS_VISIBLE |\n+---------------------------+-------------+--------------+------------+-------------+----------+------------+\n| idx_vendas_cliente_data   | id_cliente  |            1 | BTREE      |        4850 |          | YES        |\n| idx_vendas_cliente_data   | data_venda  |            2 | BTREE      |      482691 |          | YES        |\n| idx_vendas_dashboard      | data_venda  |            1 | BTREE      |      482691 |          | YES        |\n| idx_vendas_dashboard      | id_produto  |            2 | BTREE      |      479874 |          | YES        |\n| idx_vendas_dashboard      | status      |            3 | BTREE      |      499112 |          | YES        |\n| idx_vendas_dashboard      | valor_total |            4 | BTREE      |      498938 |          | YES        |\n| PRIMARY                   | id          |            1 | BTREE      |      498200 |          | YES        |\n+---------------------------+-------------+--------------+------------+-------------+----------+------------+\n7 rows in set (0.00 sec)\n<\/code><\/pre>\n<pre><code>-- 2. Obter o tamanho estimado de cada \u00edndice em MB\nSELECT\n    index_name,\n    ROUND(SUM(stat_value * @@innodb_page_size) \/ (1024 * 1024), 2) AS size_mb\nFROM mysql.innodb_index_stats\nWHERE database_name = 'aula13_indices'\n  AND table_name = 'vendas'\n  AND stat_name = 'size'\nGROUP BY index_name\nORDER BY size_mb DESC;\n<\/code><\/pre>\n<pre><code class=\"output\">+---------------------------+---------+\n| index_name                | size_mb |\n+---------------------------+---------+\n| PRIMARY                   |   42.10 |\n| idx_vendas_dashboard      |   24.75 |\n| idx_vendas_cliente_data   |   16.30 |\n+---------------------------+---------+\n3 rows in set (0.01 sec)\n<\/code><\/pre>\n<pre><code>-- 3. Verificar se o otimizador continua usando o \u00edndice correto ap\u00f3s ANALYZE\nANALYZE TABLE vendas;\n\nEXPLAIN SELECT id_cliente, data_venda, valor_total\nFROM vendas\nWHERE id_cliente = 1550\n  AND data_venda >= '2025-06-01'\nORDER BY data_venda;\n<\/code><\/pre>\n<pre><code class=\"output\">+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-----------------------+\n| id | select_type | table  | type | possible_keys           | key                     | key_len | ref         | rows | filtered | Extra                 |\n+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-----------------------+\n|  1 | SIMPLE      | vendas | ref  | idx_vendas_cliente_data | idx_vendas_cliente_data | 7       | const,const |   45 |   100.00 | Using index condition |\n+----+-------------+--------+------+-------------------------+-------------------------+---------+-------------+------+----------+-----------------------+\n<\/code><\/pre>\n<p>Se seus resultados bateram com os exibidos, o ambiente est\u00e1 corretamente configurado e os \u00edndices est\u00e3o funcionais. A presen\u00e7a de <strong>\"Using index condition\"<\/strong> indica que o MySQL est\u00e1 aplicando a otimiza\u00e7\u00e3o <strong>Index Condition Pushdown (ICP)<\/strong>, onde o pr\u00f3prio motor de armazenamento filtra as linhas usando o \u00edndice antes de envi\u00e1-las \u00e0 camada SQL \u2014 mais um n\u00edvel de efici\u00eancia que um bom \u00edndice proporciona.<\/p>\n<h3>Erros Comuns e Como Resolver<\/h3>\n<p>Durante a implementa\u00e7\u00e3o e manuten\u00e7\u00e3o de <strong>\u00edndices no MySQL<\/strong>, uma s\u00e9rie de problemas recorrentes podem desperdi\u00e7ar recursos ou at\u00e9 mesmo degradar a performance. Listamos os quatro erros mais frequentes que encontramos em campo, com suas causas, sintomas e solu\u00e7\u00f5es completas.<\/p>\n<ul>\n<li>\n        <strong>Erro #1 \u2014 \"Duplicate key name\" ao criar \u00edndice.<\/strong><br \/>\n        <em>Sintoma:<\/em> <code>ERROR 1061 (42000): Duplicate key name 'idx_nome'<\/code><br \/>\n        <em>Causa:<\/em> Tentativa de criar um \u00edndice com nome que j\u00e1 existe na tabela, mesmo que em colunas diferentes.<br \/>\n        <em>Solu\u00e7\u00e3o:<\/em> Liste os \u00edndices existentes com <code>SHOW INDEX FROM tabela;<\/code> e escolha um nome \u00fanico, ou use <code>ALTER TABLE ... ADD INDEX<\/code> sem nome (o MySQL gera um automaticamente, mas isso n\u00e3o \u00e9 recomendado para produ\u00e7\u00e3o). Em ambientes gerenciados pela JRT Technology Solutions, padronizamos prefixos como <strong>idx_<\/strong> para \u00edndices comuns e <strong>uq_<\/strong> para unique indexes, evitando colis\u00f5es.\n    <\/li>\n<li>\n        <strong>Erro #2 \u2014 \u00cdndice criado mas EXPLAIN mostra \"type: ALL\" (full scan).<\/strong><br \/>\n        <em>Sintoma:<\/em> O \u00edndice existe em <code>SHOW INDEX<\/code>, mas <code>EXPLAIN<\/code> mostra <code>key: NULL<\/code> e <code>type: ALL<\/code>.<br \/>\n        <em>Causa:<\/em> Geralmente coluna com baixa cardinalidade, uso de fun\u00e7\u00e3o sobre a coluna indexada (<code>WHERE YEAR(data_venda) = 2025<\/code>) \u2014 o que impede o uso do \u00edndice \u2014 ou coluna com NULL em grande propor\u00e7\u00e3o se for \u00edndice UNIQUE.<br \/>\n        <em>Solu\u00e7\u00e3o:<\/em> Remova fun\u00e7\u00f5es do lado da coluna no WHERE. Substitua <code>WHERE YEAR(data) = 2025<\/code> por <code>WHERE data BETWEEN '2025-01-01' AND '2025-12-31'<\/code>. Se a cardinalidade for baixa, remova o \u00edndice. Use <code>EXPLAIN FORMAT=JSON<\/code> para ver metadados detalhados da decis\u00e3o do otimizador.\n    <\/li>\n<li>\n        <strong>Erro #3 \u2014 Lentid\u00e3o extrema em INSERT\/UPDATE ap\u00f3s cria\u00e7\u00e3o de muitos \u00edndices.<\/strong><br \/>\n        <em>Sintoma:<\/em> Opera\u00e7\u00f5es de escrita que antes levavam milissegundos agora demoram segundos, com picos de I\/O em disco.<br \/>\n        <em>Causa:<\/em> Cada \u00edndice adicional em uma tabela InnoDB precisa ser atualizado a cada INSERT, UPDATE que afete colunas indexadas, e DELETE. Em tabelas com 10 ou mais \u00edndices, o overhead se torna proibitivo.<br \/>\n        <em>Solu\u00e7\u00e3o:<\/em> Audite os \u00edndices e remova aqueles que n\u00e3o s\u00e3o utilizados. Consulte a tabela <code>sys.schema_unused_indexes<\/code> (se o sys schema estiver instalado) ou monitore com <code>performance_schema.table_io_waits_summary_by_index_usage<\/code>. Em projetos de otimiza\u00e7\u00e3o, uma an\u00e1lise de workload com <strong>pt-query-digest<\/strong> da Percona \u00e9 imprescind\u00edvel.\n    <\/li>\n<li>\n        <strong>Erro #4 \u2014 \u00cdndice FULLTEXT n\u00e3o retorna resultados esperados em buscas textuais.<\/strong><br \/>\n        <em>Sintoma:<\/em> <code>SELECT * FROM artigos WHERE MATCH(titulo, corpo) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE)<\/code> retorna 0 linhas, mas voc\u00ea sabe que existem registros correspondentes.<br \/>\n        <em>Causa:<\/em> O \u00edndice FULLTEXT ignora palavras muito curtas (par\u00e2metro <code>innodb_ft_min_token_size<\/code>, padr\u00e3o 3), stopwords padr\u00e3o podem estar filtrando termos, ou a coluna n\u00e3o foi inclu\u00edda corretamente na defini\u00e7\u00e3o do \u00edndice.<br \/>\n        <em>Solu\u00e7\u00e3o:<\/em> Verifique as stopwords com <code>SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD;<\/code>. Ajuste <code>innodb_ft_min_token_size<\/code> no arquivo <code>my.cnf<\/code> e reconstrua o \u00edndice com <code>ALTER TABLE ... DROP INDEX ... , ADD FULLTEXT ...<\/code>. Teste com palavras maiores que o m\u00ednimo configurado.\n    <\/li>\n<\/ul>\n<h3>Boas Pr\u00e1ticas e Recomenda\u00e7\u00f5es da JRT Technology Solutions<\/h3>\n<p>Ap\u00f3s anos implementando e otimizando centenas de inst\u00e2ncias MySQL para clientes de diversos portes, consolidamos um conjunto de boas pr\u00e1ticas que podem evitar a maioria dos problemas com <strong>\u00edndices no MySQL<\/strong> antes mesmo que eles surjam. A primeira e mais importante: <strong>nunca crie \u00edndices sem antes analisar as queries reais da aplica\u00e7\u00e3o<\/strong>. Use logs de slow query (<code>slow_query_log<\/code>), ferramentas como <code>pt-query-digest<\/code> e o Performance Schema para identificar exatamente quais consultas est\u00e3o consumindo mais recursos. Um \u00edndice criado para uma query que roda uma vez por dia \u00e9 custo puro; o mesmo \u00edndice para uma query que roda 500 vezes por segundo \u00e9 investimento com retorno garantido.<\/p>\n<p>Siga a regra da seletividade: as colunas mais seletivas (com maior n\u00famero de valores distintos) devem vir primeiro no \u00edndice composto. Por exemplo, <strong>id_cliente<\/strong> (5.000 valores distintos) \u00e9 mais seletivo que <strong>status<\/strong> (3 valores); logo, <code>(id_cliente, status)<\/code> \u00e9 melhor que <code>(status, id_cliente)<\/code>. Evite \u00edndices redundantes \u2014 se voc\u00ea tem um \u00edndice em <code>(A, B, C)<\/code>, um \u00edndice<\/p>\n<div style=\"margin:52px 0 40px;padding:36px 28px;background:linear-gradient(135deg,#0f172a 0%,#1a2744 100%);border:2px solid #25D366;border-radius:18px;text-align:center;box-shadow:0 4px 28px rgba(37,211,102,0.18)\">\n<p style=\"margin:0 0 10px;font-size:18px;color:#ffffff;font-weight:700;line-height:1.4\">Quer aprender na pr\u00e1tica com especialistas?<\/p>\n<p style=\"margin:0 0 28px;font-size:15px;color:#94a3b8;font-weight:400;line-height:1.6\">A JRT Technology Solutions oferece treinamentos e implementa\u00e7\u00e3o de MySQL para equipes corporativas.<\/p>\n<p>  <a href=\"https:\/\/api.whatsapp.com\/send\/?phone=5521980606699&#038;text=Ol%C3%A1!%20Tenho%20interesse%20no%20treinamento%20de%20MySQL.&#038;type=phone_number&#038;app_absent=0\"\n     target=\"_blank\" rel=\"noopener noreferrer\"\n     style=\"display:inline-flex;align-items:center;gap:12px;background:#25D366;color:#ffffff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:16px;font-weight:700;padding:15px 32px;border-radius:100px;text-decoration:none;box-shadow:0 4px 16px rgba(37,211,102,0.45);letter-spacing:0.01em\"><br \/>\n    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"#ffffff\"><path d=\"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z\"\/><\/svg><br \/>\n    Falar no WhatsApp<br \/>\n  <\/a>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Aprenda a criar e usar \u00edndices no MySQL para otimizar a performance do seu banco de dados. Melhore suas consultas com dicas pr\u00e1ticas!<\/p>\n","protected":false},"author":1,"featured_media":1177,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":0,"footnotes":""},"categories":[75],"tags":[243,2010,2009,2012,2011,241],"class_list":["post-1178","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","tag-banco-de-dados-mysql","tag-como-criar-indices-mysql","tag-indices-mysql","tag-otimizacao-de-consultas-mysql","tag-performance-mysql","tag-tutorial-mysql"],"_links":{"self":[{"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/posts\/1178","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/comments?post=1178"}],"version-history":[{"count":0,"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/posts\/1178\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/media\/1177"}],"wp:attachment":[{"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/media?parent=1178"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/categories?post=1178"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jrtx.com.br\/blog\/wp-json\/wp\/v2\/tags?post=1178"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}