En una base de datos, los índices son estructuras auxiliares que mejoran la velocidad de recuperación de datos. La comparación clásica es con el índice de un libro: en vez de leer página por página, buscamos una entrada y vamos directo a la parte que nos interesa.
Cuando una tabla no tiene un índice útil para una consulta, MySQL puede verse obligado a recorrer muchas filas, o incluso toda la tabla. Ese recorrido completo suele asociarse con un full scan. En tablas pequeñas esto puede pasar desapercibido, pero en tablas grandes el costo crece mucho.
Un índice se crea sobre una o más columnas y mantiene una estructura organizada con los valores indexados y referencias que permiten llegar más rápido a las filas reales.
Un índice en MySQL es un objeto de base de datos que puede crearse sobre una o más columnas de una tabla. Su objetivo es permitir un acceso más rápido a los datos en las consultas que utilizan esas columnas.
Por ejemplo, si tenemos miles o millones de clientes y queremos buscar a uno por apellido, sin índice MySQL tendría que revisar fila por fila. Con un índice sobre apellido, puede ubicar mucho más rápido las filas candidatas.
WHERE, JOIN, ORDER BY y GROUP BY.BETWEEN, >, <, >= o <=.UNIQUE.INSERT, UPDATE y DELETE, porque al cambiar los datos también debe mantenerse el índice.WHERE, JOIN, ORDER BY o búsquedas importantes. Indexar “todo” no es una buena práctica.En MySQL, los tipos concretos dependen también del motor de almacenamiento. En la práctica, para InnoDB, la enorme mayoría de los índices que veremos se basan en B-Tree o, más precisamente, en B+Tree.
| Tipo | Motor compatible | Ideal para | No sirve bien para |
|---|---|---|---|
| B-Tree / B+Tree | InnoDB, MyISAM, MEMORY | Búsquedas exactas, por rango, ORDER BY y gran parte de los casos generales |
Búsquedas de texto completo avanzadas |
| Full-Text | InnoDB, MyISAM | Palabras clave en textos largos | Igualdad estricta o búsquedas numéricas simples |
| Hash | MEMORY | Búsquedas exactas con = o IN |
Rangos, ORDER BY, búsquedas secuenciales |
En la práctica, más del 95% de los índices que se crean en un curso inicial de MySQL serán índices tipo B-Tree.
Los índices B-Tree son árboles balanceados. Su objetivo es organizar los valores de forma jerárquica para poder buscar muy rápido sin recorrer toda la tabla.
El árbol está organizado en niveles. Hay una raíz, puede haber nodos intermedios y finalmente nodos hoja. En cada nivel, el índice descarta grandes porciones de datos hasta llegar a la ubicación probable del valor buscado.
La idea se parece a una búsqueda binaria, pero adaptada a estructuras de almacenamiento más complejas y eficientes para bases de datos.
=.<, <=, >, >=, BETWEEN.ORDER BY.Leyenda: en un índice agrupado, las hojas contienen los datos completos de la fila. En un índice no agrupado, las hojas suelen contener el valor indexado y la clave primaria.
En InnoDB, el índice agrupado forma parte de la organización principal de la tabla. Eso significa que los datos de las filas y el índice agrupado están estrechamente ligados: las hojas del árbol son, en la práctica, las filas completas. Por eso suele explicarse que “la tabla está organizada por el índice agrupado”.
El índice no agrupado se guarda en una estructura separada de la organización principal de la tabla. No contiene la fila completa, sino la clave indexada y la referencia necesaria para llegar al dato real. Por eso, cuando una consulta necesita columnas no presentes en ese índice, MySQL suele hacer una segunda navegación hacia el índice agrupado.
Este punto es clave y conviene entenderlo bien. En InnoDB, el motor necesita una forma de identificar de manera única cada fila para organizar el índice agrupado.
rowid oculto, para poder identificar las filas.Cuando una consulta usa el índice agrupado y necesita columnas de la propia fila, muchas veces llega directamente al dato completo en una sola navegación del árbol. En cambio, cuando usa un índice no agrupado, primero encuentra la clave indexada y luego necesita una segunda búsqueda para ir al índice agrupado y obtener el resto de la fila. A esto suele explicárselo como una especie de doble salto o lookup.
Un índice compuesto es un índice definido sobre dos o más columnas.
CREATE INDEX idx_apellido_nombre ON clientes (apellido, nombre);
El orden de las columnas es crítico. Si el índice está definido como (apellido, nombre), puede servir para:
apellido;apellido y nombre;apellido y además se ordena por nombre.Pero no sirve igual de bien para búsquedas solo por nombre, porque el árbol está ordenado primero por apellido.
Si definimos una clave primaria compuesta, por ejemplo (sucursal_id, cliente_id), esa combinación pasa a ser la clave que identifica físicamente las filas en el índice agrupado.
Además, los índices secundarios deben guardar esa PK compuesta en sus hojas. Eso significa que:
UNIQUE)Un índice único garantiza que no existan dos filas con el mismo valor en la columna o conjunto de columnas indexadas.
CREATE UNIQUE INDEX idx_email_unico ON clientes (email);
También puede aparecer al definir una restricción UNIQUE dentro de la tabla.
Además de acelerar búsquedas, los índices únicos cumplen una función de integridad: impiden duplicados no permitidos.
SELECT * FROM clientes WHERE apellido = 'González';
Si no existe un índice útil sobre apellido, MySQL deberá revisar muchas filas de la tabla para encontrar coincidencias.
CREATE INDEX idx_apellido ON clientes (apellido);
SELECT * FROM clientes WHERE apellido = 'González';
Ahora MySQL puede usar el índice para localizar mucho más rápido las filas donde apellido = 'González'.
EXPLAIN es una herramienta fundamental para analizar cómo MySQL planea ejecutar una consulta. Antes de un SELECT, se puede anteponer EXPLAIN para observar si MySQL va a usar un índice, qué tabla lee, qué tipo de acceso utilizará y cuántas filas estima revisar.
EXPLAIN
SELECT *
FROM clientes
WHERE apellido = 'González';
1.SELECT es. En una consulta simple suele aparecer SIMPLE.NULL.Using where, uso de temporales o de ordenamientos adicionales.EXPLAIN aparece un índice posible pero en key no se usa ninguno, eso invita a investigar. Tal vez la consulta, el orden de columnas del índice, la selectividad o el tipo de condición hacen que MySQL decida no usarlo.Este script sirve para laboratorio o práctica en clase. La idea es crear una tabla sencilla, cargar registros, ejecutar una consulta sin índice, observar EXPLAIN, luego crear el índice y volver a ejecutar exactamente la misma consulta.
CREATE DATABASE IF NOT EXISTS prueba_indices;
USE prueba_indices;
DROP TABLE IF EXISTS clientes;
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
apellido VARCHAR(50) NOT NULL,
nombre VARCHAR(50) NOT NULL,
ciudad VARCHAR(50),
edad INT
);
INSERT INTO clientes (apellido, nombre, ciudad, edad) VALUES
('González', 'Ana', 'Mar del Plata', 21),
('Pérez', 'Juan', 'Batán', 19),
('Gómez', 'Lucía', 'Mar del Plata', 25),
('Rodríguez', 'Carlos', 'Miramar', 31),
('Fernández', 'Marta', 'Mar del Plata', 28),
('López', 'Sofía', 'Necochea', 24),
('González', 'Pedro', 'Balcarce', 36),
('Martínez', 'Laura', 'Mar del Plata', 22),
('González', 'Diego', 'Batán', 27),
('Sánchez', 'Paula', 'Miramar', 20),
('Díaz', 'Marina', 'Mar del Plata', 26),
('González', 'Nora', 'Necochea', 33);
EXPLAIN
SELECT *
FROM clientes
WHERE apellido = 'González';
En esta primera imagen se ve un escenario sin un índice útil sobre apellido. La consulta puede ejecutarse, pero MySQL no dispone todavía de una estructura específica para llegar rápido a las filas buscadas.
| Campo | Valor antes del índice | Qué significa |
|---|---|---|
id | 1 | La consulta tiene una sola etapa principal de ejecución. |
select_type | SIMPLE | Es una consulta simple, sin subconsultas ni uniones complejas. |
table | clientes | La tabla analizada en este paso es clientes. |
partitions | NULL | La tabla no está particionada o no intervienen particiones en este caso. |
type | ALL | Es uno de los peores tipos de acceso en términos de eficiencia. Significa recorrido completo de la tabla. |
possible_keys | NULL | MySQL no ve ningún índice disponible que pueda ayudar en esta consulta. |
key | NULL | No se usa ningún índice porque no existe uno útil para esta condición. |
key_len | NULL | Como no se usa índice, no hay longitud de clave aplicada. |
ref | NULL | No hay comparación contra un índice. |
rows | 12 | MySQL estima que debe revisar 12 filas. En esta tabla son pocas, pero en una tabla grande el impacto sería mucho mayor. |
filtered | 10.00 | Estimación de qué porcentaje de filas revisadas pasará el filtro. Aquí el optimizador estima que solo una parte pequeña de lo recorrido servirá. |
Extra | Using where | MySQL aplica la condición WHERE para filtrar filas después de leerlas. |
CREATE INDEX idx_clientes_apellido
ON clientes (apellido);
En MySQL, salvo indicación contraria y según el motor, este índice será de tipo B-Tree, adecuado para búsquedas exactas como apellido = 'González'.
EXPLAIN
SELECT *
FROM clientes
WHERE apellido = 'González';
Ahora sí existe un índice sobre apellido y MySQL puede aprovecharlo. Se ve claramente que el plan de ejecución cambió.
| Campo | Valor después del índice | Qué significa |
|---|---|---|
id | 1 | La consulta sigue siendo una única etapa principal. |
select_type | SIMPLE | La forma general de la consulta no cambió. |
table | clientes | La tabla sigue siendo la misma. |
partitions | NULL | No hay particiones involucradas. |
type | ref | Este tipo de acceso es mucho mejor que ALL. Indica que MySQL usa un índice no único para encontrar filas que coinciden con un valor de búsqueda. |
possible_keys | idx_clientes_apellido | Ahora sí existe un índice que podría ayudar a resolver la consulta. |
key | idx_clientes_apellido | MySQL eligió efectivamente ese índice. |
key_len | 202 | MySQL informa la longitud de clave que está utilizando. Este dato depende del tipo de columna, la codificación y cómo el motor representa internamente el acceso. |
ref | const | La comparación del índice se hace contra un valor constante, en este caso el literal 'González'. |
rows | 4 | Ahora estima revisar solo 4 filas, es decir, las filas candidatas encontradas por el índice, en lugar de recorrer toda la tabla. |
filtered | 100.00 | De las filas candidatas encontradas por el índice, el optimizador estima que todas cumplen el filtro. Tiene sentido porque el índice ya llevó directamente a los apellidos buscados. |
Extra | NULL | No hace falta informar un filtrado adicional destacado como en el caso anterior; el acceso ya quedó mucho más afinado gracias al índice. |
| Campo | Antes | Después | Lectura didáctica |
|---|---|---|---|
type | ALL | ref | Se pasó de un recorrido completo de tabla a un acceso por índice. |
possible_keys | NULL | idx_clientes_apellido | Antes no había un índice candidato; después sí. |
key | NULL | idx_clientes_apellido | Antes no se usaba índice; después MySQL usa el creado. |
rows | 12 | 4 | La estimación de filas a revisar bajó de manera clara. |
ref | NULL | const | Después del índice, la búsqueda se apoya en una comparación directa contra un valor constante. |
Extra | Using where | NULL | En el primer caso el filtrado se hace luego de leer toda la tabla; en el segundo, el acceso ya está mucho más restringido gracias al índice. |
EXPLAIN no devuelve los datos finales de la consulta. Lo que devuelve es el plan de ejecución estimado, es decir, cómo piensa trabajar MySQL para obtener esos datos.EXPLAIN
SELECT id, apellido, nombre
FROM clientes
WHERE apellido = 'González'
ORDER BY nombre;
En este caso, el índice sobre apellido puede ayudar al filtrado, pero no necesariamente resolver por sí solo el ordenamiento por nombre. Aquí aparece la utilidad de los índices compuestos.
CREATE INDEX idx_clientes_apellido_nombre
ON clientes (apellido, nombre);
EXPLAIN
SELECT id, apellido, nombre
FROM clientes
WHERE apellido = 'González'
ORDER BY nombre;
Esta prueba permite ver por qué el orden de las columnas dentro del índice compuesto importa tanto. Si primero filtramos por apellido y después ordenamos por nombre, un índice (apellido, nombre) puede resultar muy conveniente.
Los índices Full-Text están diseñados para búsquedas en grandes bloques de texto. No son simplemente una versión rápida de LIKE '%palabra%'; trabajan de otra manera.
En lugar de buscar coincidencias carácter por carácter en todo el texto, el motor tokeniza el contenido, es decir, lo divide en palabras o unidades llamadas tokens. Luego las almacena en una estructura que permite búsquedas por relevancia.
Un token es una unidad elemental de información. En un índice Full-Text, normalmente un token es una palabra relevante extraída del texto.
Didácticamente puede explicarse como una estructura interna asociada a la tabla que relaciona tokens con filas. No es la misma organización que un B-Tree clásico de igualdad o rango. Lo importante es entender que el motor mantiene un índice especializado para palabras y referencias a los registros donde aparecen.
Si una fila contiene el texto “Los avances en la inteligencia artificial son sorprendentes”, el sistema puede quedarse con tokens como avances, inteligencia, artificial y sorprendentes, descartando otras palabras comunes.
CREATE TABLE articulos (
id INT PRIMARY KEY,
titulo VARCHAR(200),
contenido TEXT,
FULLTEXT(titulo, contenido)
);
SELECT *
FROM articulos
WHERE MATCH(titulo, contenido) AGAINST('base de datos');
También puede crearse con una instrucción separada:
CREATE FULLTEXT INDEX idx_contenido ON articulos (contenido);
LIKE '%texto%' cuando el objetivo es buscar contenido textual grande.Un índice Hash utiliza una función hash para convertir un valor en un identificador y localizar el dato asociado de forma directa. No organiza los datos por orden, sino por el resultado del hash.
En el caso típico de tablas MEMORY, los índices y los datos se mantienen en memoria RAM mientras la tabla exista y el servidor permanezca activo. Esto explica su gran velocidad y también una de sus limitaciones más importantes: su comportamiento está pensado para datos temporales o altamente volátiles, no para persistencia clásica.
Son extremadamente rápidos para búsquedas de igualdad, como = o IN.
No tienen orden lógico. Por eso no sirven para:
BETWEEN;<, >;ORDER BY.CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255),
precio DECIMAL(10, 2),
tipo_memoria VARCHAR(50)
) ENGINE=MEMORY;
CREATE INDEX idx_tipo_hash ON productos (tipo_memoria) USING HASH;
SELECT * FROM productos WHERE tipo_memoria = 'SSD';
Una colisión ocurre cuando dos valores distintos producen el mismo hash. El motor puede resolverlo, pero si hay muchas colisiones el rendimiento puede deteriorarse.
MEMORY.ORDER BY.El motor MEMORY almacena sus datos en RAM. Eso lo hace muy rápido, pero también implica limitaciones importantes. Didácticamente conviene pensar que no es el motor común para una base transaccional persistente, sino una herramienta para casos muy puntuales.
Los índices también requieren mantenimiento. Con el tiempo, inserciones, actualizaciones y eliminaciones pueden afectar su organización y el rendimiento general.
OPTIMIZE TABLE nombre_tabla;
Esta operación puede ayudar a reorganizar y optimizar la estructura asociada a la tabla y sus índices, dependiendo del motor.
MySQL no permite, en general, cambiar directamente las columnas de un índice existente con una instrucción tipo “editar índice”. Lo habitual es:
DROP INDEX nombre_indice ON nombre_tabla;
CREATE INDEX nuevo_nombre_indice
ON nombre_tabla (nueva_columna1, nueva_columna2);
DROP INDEX nombre_indice ON nombre_tabla;
EXPLAIN antes y después de cambios importantes.JOIN frecuentes, pensar el indexado de manera explícita.Conviene recordar que una FK sin buen soporte de indexación puede volver lentos los JOIN y ciertas validaciones. En la práctica, siempre es recomendable revisar cómo quedaron indexadas las columnas implicadas en relaciones.
1. ¿Por qué los B-Tree sirven de base para muchos índices?
Porque ofrecen una estructura versátil: soportan igualdad, rangos y ordenamiento. Por eso muchos motores los usan como base para gran parte de sus índices generales.
2. ¿Qué sucede al declarar una PK?
En InnoDB, normalmente esa PK pasa a ser el índice agrupado de la tabla.
3. ¿Qué pasa si no declaramos PK?
El motor necesita igualmente una forma de identificar filas. Puede usar una clave UNIQUE NOT NULL adecuada y, si tampoco existe, crear una clave interna oculta comparable a un rowid.
4. ¿Qué diferencia hay para el motor al leer usando un clustered o un non-clustered?
Con el clustered suele poder llegar más directo a la fila completa. Con el non-clustered primero encuentra la referencia y luego debe buscar la fila real en el índice agrupado.
5. ¿Por qué un índice no agrupado apunta al agrupado?
Porque el agrupado representa la organización principal de las filas. Así el índice secundario no necesita duplicar todos los datos completos de la fila.
6. ¿Qué impacto tiene que el motor termine usando un rowid oculto?
Se pierde claridad de diseño y dejamos una decisión importante en manos del motor. Por eso es mucho mejor definir una PK explícita y estable.
7. ¿Cómo afectan los índices a las FK?
Un buen indexado sobre columnas relacionadas ayuda enormemente al rendimiento de JOIN, búsquedas y controles asociados a integridad referencial.
8. ¿Cómo impacta una PK compuesta?
Afecta el orden físico del índice agrupado y también el tamaño de índices secundarios, porque estos deben almacenar esa PK compuesta como referencia.
La idea central es simple: los índices aceleran lecturas, pero tienen costo de mantenimiento. Por eso deben diseñarse en función de consultas reales. Entender la diferencia entre buscar datos, ordenar datos y mantener datos ayuda a pensar la base de datos con criterio y no solo desde la sintaxis.