EXPLAIN

Cuando una consulta tarda más de lo esperado, el primer paso para entender qué ocurre es pedirle a MySQL que te muestre su plan de ejecución. La sentencia EXPLAIN hace exactamente eso: en lugar de ejecutar la consulta, MySQL te dice cómo piensa ejecutarla. Te muestra qué tablas va a leer, en qué orden, si usará índices o recorrerá todas las filas, y qué estrategia aplicará para resolver joins y subconsultas. Con esta información puedes identificar dónde está el problema y decidir si necesitas crear un índice, reescribir la consulta o reestructurar tus datos.

Sin EXPLAIN estarías optimizando a ciegas. Podrías añadir índices que MySQL nunca usa, o perder horas reescribiendo una consulta cuyo verdadero cuello de botella es un full table scan en una tabla auxiliar. EXPLAIN te da visibilidad sobre las decisiones internas del optimizador y convierte la optimización en un proceso metódico en lugar de adivinanza.

Sintaxis básica

La forma más directa de usar EXPLAIN es colocarlo delante de cualquier sentencia SELECT:

EXPLAIN SELECT * FROM pedidos WHERE total > 500;

MySQL no ejecuta la consulta. En su lugar, analiza la sentencia, consulta las estadísticas de las tablas y los índices disponibles, y devuelve una tabla con el plan de ejecución. Cada fila del resultado representa una tabla o subconsulta involucrada en el plan.

También puedes usar la palabra DESCRIBE como sinónimo de EXPLAIN, ya que MySQL los trata de forma idéntica en este contexto:

DESCRIBE SELECT nombre, email FROM clientes WHERE ciudad = 'Madrid';

Ambas formas producen exactamente la misma salida.

Columnas del resultado

Cuando ejecutas un EXPLAIN básico, MySQL devuelve una tabla con varias columnas. Entender cada una es fundamental para interpretar el plan de ejecución correctamente.

EXPLAIN SELECT c.nombre, p.total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.total > 200;
idselect_typetabletypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEpALLcliente_idNULLNULLNULL5033.33Using where
1SIMPLEceq_refPRIMARYPRIMARY4tienda_mysql.p.cliente_id1100.00NULL

La columna id identifica cada paso del plan. Cuando todas las filas tienen el mismo id, se ejecutan como un único bloque (un join, por ejemplo). Si hay ids diferentes, los valores más altos se ejecutan primero.

La columna select_type indica el tipo de consulta. SIMPLE significa que no hay subconsultas ni uniones; SUBQUERY aparece cuando hay una subconsulta en el WHERE o en el SELECT; DERIVED indica una subconsulta en el FROM.

La columna table muestra la tabla o alias que MySQL lee en ese paso. La columna type es posiblemente la más importante: indica cómo MySQL accede a la tabla. Los valores van de mejor a peor rendimiento: system y const (una sola fila), eq_ref (join con clave primaria), ref (join con índice no único), range (rango de un índice), index (escaneo completo del índice) y ALL (full table scan, el peor caso).

La columna possible_keys lista los índices que MySQL podría usar, mientras que key muestra cuál eligió realmente. key_len indica cuántos bytes del índice se utilizan, lo que es útil para saber si un índice compuesto se usa parcial o completamente. ref muestra qué columna o constante se compara con el índice.

La columna rows es una estimación del número de filas que MySQL examinará. No es el número exacto, sino una aproximación basada en las estadísticas de la tabla. filtered indica el porcentaje estimado de filas que pasarán la condición WHERE después del filtrado. Finalmente, Extra contiene información adicional como Using where, Using index (cobertura de índice), Using temporary (tabla temporal) o Using filesort (ordenación externa).

Full table scan vs uso de índice

La diferencia más evidente que EXPLAIN te permite detectar es si MySQL recorre toda la tabla o aprovecha un índice. Veamos un ejemplo concreto con la tabla productos.

EXPLAIN SELECT * FROM productos WHERE nombre = 'iPhone 15 Pro';
idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEproductosALLNULLNULL30Using where

El valor ALL en la columna type confirma un full table scan. MySQL lee las 30 filas de la tabla para encontrar las que coinciden. La columna possible_keys muestra NULL, lo que significa que no existe ningún índice que pueda ayudar con esta consulta. En una tabla pequeña de 30 filas esto es insignificante, pero imagina una tabla con millones de registros: cada consulta obligaría a MySQL a recorrerla entera.

Ahora creamos un índice sobre la columna nombre y repetimos:

CREATE INDEX idx_nombre ON productos (nombre);
 
EXPLAIN SELECT * FROM productos WHERE nombre = 'iPhone 15 Pro';
idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEproductosrefidx_nombreidx_nombre1NULL

El cambio es radical. type pasa de ALL a ref, key muestra que usa idx_nombre y rows baja de 30 a 1. MySQL va directamente a la fila que necesita gracias al índice. Esta es la esencia de la optimización: usar EXPLAIN para detectar full table scans y resolverlos con índices apropiados.

EXPLAIN con joins

Cuando una consulta involucra múltiples tablas, EXPLAIN muestra una fila por cada tabla y te permite ver el orden en que MySQL las procesa. Este orden no siempre coincide con el que escribiste en la consulta, ya que el optimizador puede reordenar los joins para mejorar el rendimiento.

EXPLAIN SELECT c.nombre, c.email, p.fecha, p.total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
JOIN detalle_pedidos dp ON dp.pedido_id = p.id
JOIN productos pr ON pr.id = dp.producto_id
WHERE pr.precio > 1000;
idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEprALLPRIMARYNULL30Using where
1SIMPLEdprefpedido_id,producto_idproducto_id2NULL
1SIMPLEpeq_refPRIMARY,cliente_idPRIMARY1NULL
1SIMPLEceq_refPRIMARYPRIMARY1NULL

Observa que MySQL eligió empezar por productos (alias pr) y filtrar por precio, aunque en nuestra consulta la escribimos al final en el FROM. Esto ocurre porque el optimizador estimó que filtrar primero los productos caros reduce significativamente el número de filas que tendrá que procesar en los joins posteriores. Cada join subsiguiente usa eq_ref (clave primaria), lo que es óptimo.

Si ves ALL en una tabla intermedia de un join con muchas filas, es señal de que falta un índice. La solución suele ser crear un índice en la columna de join correspondiente.

EXPLAIN FORMAT=JSON

El formato tabular es compacto pero puede resultar limitado para consultas complejas. MySQL ofrece un formato JSON que incluye información mucho más detallada, como el coste estimado de cada operación.

EXPLAIN FORMAT=JSON
SELECT c.nombre, COUNT(*) AS total_pedidos
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.nombre
HAVING total_pedidos > 2;

La salida JSON incluye campos como query_cost (coste total estimado de la consulta), read_cost y eval_cost para cada tabla, rows_examined_per_scan y rows_produced_per_join. Estos valores de coste son unidades internas del optimizador y no se corresponden directamente con tiempo real, pero son útiles para comparar planes alternativos de una misma consulta: si reescribes una consulta y el query_cost baja, generalmente indica una mejora.

El formato JSON también es especialmente útil cuando trabajas con herramientas gráficas como MySQL Workbench, que pueden leer este JSON y mostrarlo como un diagrama visual del plan de ejecución.

EXPLAIN FORMAT=TREE

A partir de MySQL 8.0.16, puedes obtener el plan de ejecución en formato de árbol, que muestra la jerarquía de operaciones de forma más legible que la tabla clásica.

EXPLAIN FORMAT=TREE
SELECT c.nombre, p.total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.fecha >= '2026-01-01'
ORDER BY p.total DESC
LIMIT 10;
-> Limit: 10 row(s)
    -> Sort: p.total DESC, limit input to 10 row(s) per chunk
        -> Nested loop join
            -> Filter: (p.fecha >= '2026-01-01')  (cost=5.25 rows=16)
                -> Table scan on p  (cost=5.25 rows=50)
            -> Single-row index lookup on c using PRIMARY (id=p.cliente_id)  (cost=0.27 rows=1)

El formato de árbol se lee de adentro hacia afuera (o de abajo hacia arriba). Las operaciones más internas son las primeras en ejecutarse. En este caso, MySQL primero escanea la tabla pedidos, filtra por fecha, y luego para cada fila resultante busca el cliente correspondiente usando la clave primaria. Finalmente ordena por total y aplica el límite.

Este formato muestra el coste estimado y las filas esperadas de cada operación, lo que facilita identificar cuál es la parte más cara de la consulta. Si ves un Table scan con un coste alto en una tabla grande, es un candidato claro para añadir un índice.

EXPLAIN en UPDATE, DELETE e INSERT

EXPLAIN no se limita a consultas SELECT. También puedes usarlo con sentencias UPDATE, DELETE e INSERT ... SELECT, lo que resulta especialmente útil cuando una operación de modificación es lenta y no sabes por qué.

EXPLAIN UPDATE pedidos
SET estado = 'procesado'
WHERE fecha < '2025-06-01' AND estado = 'pendiente';
idselect_typetabletypepossible_keyskeyrowsExtra
1UPDATEpedidosALLNULLNULL50Using where

El full table scan (ALL) en un UPDATE indica que MySQL debe examinar todas las filas para encontrar las que cumplen ambas condiciones. Si la tabla pedidos tiene millones de filas, este UPDATE será muy lento. La solución sería crear un índice compuesto sobre (estado, fecha) para que MySQL localice directamente las filas afectadas.

EXPLAIN DELETE FROM detalle_pedidos
WHERE pedido_id IN (
    SELECT id FROM pedidos WHERE fecha < '2025-01-01'
);

En este caso, EXPLAIN muestra tanto la consulta principal (el DELETE) como la subconsulta que busca los pedidos antiguos. Si la subconsulta hace un full table scan, aunque el DELETE use un índice, el rendimiento global se verá afectado.

Estimaciones vs realidad

Es importante recordar que EXPLAIN muestra estimaciones, no datos reales. La columna rows se basa en las estadísticas que MySQL mantiene sobre cada tabla, y estas estadísticas pueden estar desactualizadas. Esto ocurre especialmente después de inserciones o eliminaciones masivas.

Puedes actualizar las estadísticas manualmente con:

ANALYZE TABLE pedidos;
ANALYZE TABLE clientes;

Después de ejecutar ANALYZE TABLE, las estadísticas se recalculan y las estimaciones de EXPLAIN serán más precisas. Aun así, las estimaciones nunca serán exactas. Para conocer los valores reales de filas examinadas y tiempos de ejecución, necesitas EXPLAIN ANALYZE, disponible desde MySQL 8.0.18.

Otra limitación es que EXPLAIN no considera la caché del sistema operativo ni del buffer pool de InnoDB. Una consulta puede parecer costosa según EXPLAIN pero ejecutarse rápidamente porque todos los datos están en memoria. Esto no significa que debas ignorar lo que EXPLAIN te dice: si la tabla crece y ya no cabe en memoria, esos full table scans que parecían inofensivos se convertirán en un problema serio.

Valores clave de la columna type

La columna type del resultado de EXPLAIN es el indicador más rápido del rendimiento de una consulta. Conocer los valores posibles te permite evaluar la eficiencia de un vistazo.

El valor const aparece cuando MySQL puede resolver la consulta leyendo exactamente una fila, normalmente al buscar por clave primaria con un valor constante. Es el caso más eficiente posible.

EXPLAIN SELECT * FROM clientes WHERE id = 5;
idselect_typetabletypepossible_keyskeyrowsExtra
1SIMPLEclientesconstPRIMARYPRIMARY1NULL

El valor ref indica que MySQL usa un índice no único para encontrar las filas. Es habitual en joins y en búsquedas por columnas indexadas que admiten duplicados.

El valor range aparece cuando MySQL usa un índice para escanear un rango de valores, como en consultas con BETWEEN, >, < o IN con pocos valores.

EXPLAIN SELECT * FROM pedidos WHERE total BETWEEN 100 AND 500;

Si hay un índice sobre total, verás range en la columna type. Sin índice, será ALL.

El valor index significa que MySQL lee el índice completo, no la tabla. Es mejor que ALL porque los índices son más pequeños, pero sigue siendo un escaneo completo. El valor ALL es el peor caso: full table scan, leer cada fila de la tabla.

Errores comunes al usar EXPLAIN

Uno de los errores más frecuentes es interpretar la columna rows como el número exacto de filas del resultado. rows es una estimación de las filas que MySQL examinará, no las que devolverá. Una consulta puede examinar 10,000 filas según EXPLAIN pero devolver solo 3. La columna filtered te ayuda a estimar cuántas filas pasarán el filtro, pero sigue siendo una aproximación.

Otro error habitual es asumir que possible_keys con valores significa que se está usando un índice. Lo que importa es la columna key: si key es NULL, MySQL no usa ningún índice a pesar de que existan candidatos. Esto puede ocurrir cuando el optimizador estima que un full table scan es más eficiente que usar el índice, algo que sucede cuando la consulta devuelve un porcentaje alto de las filas de la tabla.

También es un error común ejecutar EXPLAIN solo en el entorno de desarrollo con pocas filas. El optimizador puede elegir planes muy diferentes en producción, donde las tablas tienen millones de registros. Siempre que sea posible, ejecuta EXPLAIN contra una base de datos con un volumen de datos representativo.

Cuándo usar EXPLAIN

Deberías usar EXPLAIN siempre que una consulta sea más lenta de lo esperado. Pero también tiene sentido usarlo de forma preventiva: antes de desplegar una nueva consulta a producción, revisa su plan de ejecución para asegurarte de que usa índices adecuados.

En resumen, EXPLAIN es la primera herramienta de diagnóstico cuando trabajas en optimización de consultas. Te muestra el plan que MySQL tiene para ejecutar tu consulta y te permite detectar problemas antes de que se conviertan en cuellos de botella. Cuando necesites ir más allá y medir los tiempos reales de ejecución, el siguiente paso es EXPLAIN ANALYZE. Y si quieres un análisis temporal detallado de cada fase interna, puedes complementar con profiling.

Escrito por Eduardo Lázaro