Interpretar EXPLAIN
Ejecutar EXPLAIN delante de una consulta es solo el primer paso. El verdadero valor está en saber leer cada columna del resultado, porque cada una revela un aspecto distinto de cómo MySQL planifica la ejecución. Una fila con type: ALL y rows: 450000 te dice que hay un full table scan sobre casi medio millón de filas, mientras que otra con type: ref y rows: 3 indica un acceso quirúrgico por índice. Entender estas diferencias es lo que separa una optimización superficial de una realmente efectiva.
En este artículo desglosamos columna por columna el resultado de EXPLAIN, con ejemplos realistas sobre tablas de clientes, pedidos y productos. Si todavía no has ejecutado tu primer EXPLAIN, empieza por la guía introductoria de EXPLAIN y vuelve aquí cuando necesites interpretar los resultados en profundidad.
Estructura general del resultado
Cuando ejecutas EXPLAIN sobre una consulta, MySQL devuelve una tabla donde cada fila representa una operación sobre una tabla o subexpresión. Las columnas que verás son las siguientes:
EXPLAIN SELECT c.nombre, COUNT(p.id) AS total_pedidos
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id
WHERE c.ciudad = 'Madrid'
GROUP BY c.id;| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | c | ref | PRIMARY,idx_ciudad | idx_ciudad | 102 | const | 45 | 100.00 | Using index condition |
| 1 | SIMPLE | p | ref | idx_cliente_id | idx_cliente_id | 4 | tienda.c.id | 8 | 100.00 | NULL |
Cada fila corresponde a un paso en el plan de ejecución. En este ejemplo, MySQL primero accede a la tabla clientes filtrando por el índice de ciudad, y luego para cada cliente encontrado busca sus pedidos mediante el índice sobre cliente_id. Veamos cada columna en detalle.
Columna id
La columna id identifica cada SELECT dentro de la consulta. En una consulta simple sin subconsultas ni UNION, todas las filas comparten el mismo id (normalmente 1). Cuando hay subconsultas o tablas derivadas, cada nivel de anidamiento recibe un id distinto, y los valores más altos se ejecutan primero.
EXPLAIN SELECT nombre, (
SELECT COUNT(*) FROM pedidos WHERE pedidos.cliente_id = clientes.id
) AS total_pedidos
FROM clientes
WHERE ciudad = 'Barcelona';| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | PRIMARY | clientes | ref | idx_ciudad | idx_ciudad | 32 | Using index condition |
| 2 | DEPENDENT SUBQUERY | pedidos | ref | idx_cliente_id | idx_cliente_id | 8 | Using index |
Aquí el id: 2 corresponde a la subconsulta correlacionada que se ejecuta para cada fila del SELECT principal (id: 1). Cuando ves varios IDs, piensa en ellos como capas: MySQL resuelve las capas internas antes que las externas. Si dos filas comparten el mismo id, pertenecen al mismo nivel de ejecución y MySQL las procesa conjuntamente, como ocurre con las dos tablas de un INNER JOIN.
Columna select_type
Esta columna describe el papel que juega cada SELECT dentro de la consulta global. MySQL necesita distinguir entre el SELECT principal, las subconsultas, las tablas derivadas y las uniones, porque cada una se ejecuta de forma diferente internamente.
Los valores más habituales son estos:
SIMPLE aparece cuando la consulta no contiene subconsultas ni UNION. Es el caso más frecuente y el más eficiente, porque MySQL puede optimizar toda la consulta como una sola unidad.
EXPLAIN SELECT * FROM productos WHERE precio > 50;| id | select_type | table | type | key | rows |
|---|---|---|---|---|---|
| 1 | SIMPLE | productos | range | idx_precio | 18 |
PRIMARY identifica al SELECT más externo cuando existen subconsultas o UNION. Es el punto de entrada de la ejecución.
SUBQUERY aparece para subconsultas independientes en el SELECT o en el WHERE que no dependen de la consulta externa. MySQL las ejecuta una sola vez y reutiliza el resultado.
DEPENDENT SUBQUERY marca una subconsulta correlacionada que se reevalúa para cada fila de la consulta exterior. Estas son las más costosas, porque se ejecutan tantas veces como filas devuelve el SELECT principal.
DERIVED se asigna a las tablas derivadas (subconsultas en el FROM). MySQL materializa el resultado en una tabla temporal antes de usarla en el JOIN o filtro exterior. Muchas veces puedes sustituir una tabla derivada por una CTE para mejorar la legibilidad sin cambiar el rendimiento.
UNION y UNION RESULT aparecen en consultas con UNION. Cada SELECT después del primero recibe el tipo UNION, y la fila que combina todos los resultados parciales se marca como UNION RESULT.
Columna table
La columna table indica sobre qué tabla opera cada fila del plan. En la mayoría de los casos muestra directamente el nombre de la tabla o su alias. Sin embargo, hay situaciones donde el valor tiene un formato especial.
Cuando ves <derivedN>, significa que MySQL está accediendo a una tabla temporal generada por la subconsulta con id = N. Por ejemplo, <derived2> es el resultado materializado de la subconsulta con id: 2. Del mismo modo, <unionM,N> representa la tabla temporal que combina los resultados de los SELECT con IDs M y N en una operación UNION.
EXPLAIN SELECT * FROM (
SELECT categoria_id, AVG(precio) AS precio_medio
FROM productos
GROUP BY categoria_id
) AS medias
WHERE precio_medio > 100;| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | PRIMARY | <derived2> | ALL | NULL | 12 | Using where |
| 2 | DERIVED | productos | index | idx_categoria | 150 | NULL |
La primera fila accede a la tabla derivada generada por la subconsulta de id: 2, que agrupa productos por categoría.
Columna type: el indicador más importante
De todas las columnas del EXPLAIN, type es la que más impacto tiene en el rendimiento. Describe el método de acceso que MySQL usa para leer filas de la tabla. Los valores van desde los más eficientes (acceso a una sola fila) hasta los más costosos (lectura completa de la tabla). Conocer esta escala te permite detectar problemas de rendimiento de un solo vistazo.
system y const
Estos son los tipos de acceso más rápidos posibles. system aparece cuando la tabla tiene exactamente una fila (como una tabla del sistema). const se produce cuando MySQL puede resolver la condición del WHERE contra una clave primaria o un índice UNIQUE con un valor constante, lo que garantiza que como máximo habrá una fila.
EXPLAIN SELECT * FROM clientes WHERE id = 42;| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | clientes | const | PRIMARY | 1 | NULL |
MySQL sabe antes de ejecutar la consulta que la clave primaria id = 42 solo puede devolver una fila. No necesita estimar nada: va directamente a esa posición en el índice.
eq_ref
Este tipo aparece en JOINs cuando MySQL usa un índice de clave primaria o UNIQUE para buscar exactamente una fila en la tabla unida por cada fila de la tabla anterior. Es el mejor tipo de acceso posible para un JOIN.
EXPLAIN SELECT p.fecha_pedido, c.nombre, c.email
FROM pedidos p
INNER JOIN clientes c ON c.id = p.cliente_id;| id | select_type | table | type | key | ref | rows |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | p | ALL | NULL | NULL | 500 |
| 1 | SIMPLE | c | eq_ref | PRIMARY | tienda.p.cliente_id | 1 |
Para cada pedido, MySQL busca el cliente correspondiente usando la clave primaria. La columna rows: 1 confirma que cada búsqueda devuelve como máximo una fila. Este patrón es extremadamente eficiente incluso con millones de filas.
ref
El tipo ref indica que MySQL usa un índice no único para buscar filas que coinciden con un valor concreto. A diferencia de eq_ref, puede devolver varias filas por cada búsqueda, porque el índice no garantiza unicidad. Aun así, es un acceso muy eficiente.
EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 42;| id | select_type | table | type | key | ref | rows |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | ref | idx_cliente_id | const | 8 |
MySQL navega el índice idx_cliente_id hasta encontrar las filas con valor 42 y lee solo esas 8 filas estimadas. Si la tabla tiene 500.000 pedidos, leer 8 en lugar de medio millón supone una diferencia abismal.
range
El acceso range se produce cuando MySQL usa un índice para recuperar filas dentro de un rango definido por operadores como >, <, >=, <=, BETWEEN o IN. MySQL recorre el índice desde el punto de inicio hasta el punto final del rango, sin leer el resto.
EXPLAIN SELECT * FROM pedidos
WHERE fecha_pedido BETWEEN '2026-01-01' AND '2026-01-31';| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | range | idx_fecha_pedido | 1200 | Using index condition |
El rendimiento de un range depende de cuántas filas cubre el rango. Un rango estrecho sobre un índice selectivo es casi tan rápido como un ref; un rango que cubre el 90% de la tabla es poco mejor que un full scan.
index
El tipo index significa que MySQL lee el índice completo, de principio a fin, en lugar de la tabla completa. Es parecido a un full table scan, pero sobre el índice, que generalmente es más pequeño que la tabla. Aparece cuando MySQL necesita recorrer todas las filas pero puede obtener los datos exclusivamente del índice sin tocar la tabla (un covering index).
EXPLAIN SELECT cliente_id, COUNT(*) FROM pedidos GROUP BY cliente_id;| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | index | idx_cliente_id | 5000 | Using index |
Aunque recorre las 5.000 filas del índice, no necesita acceder a la tabla porque cliente_id ya está en el índice. Eso lo hace más rápido que un ALL, pero sigue siendo un recorrido completo.
ALL
Este es el tipo de acceso más lento. MySQL lee la tabla entera, fila por fila, sin usar ningún índice. En tablas pequeñas puede ser aceptable, pero en tablas con miles o millones de filas es una señal clara de que falta un índice.
EXPLAIN SELECT * FROM productos WHERE descripcion LIKE '%oferta%';| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | productos | ALL | NULL | 150 | Using where |
El patrón LIKE '%oferta%' con comodín al inicio impide el uso de cualquier índice B-tree, forzando un full table scan. Para búsquedas de texto dentro de cadenas, considera usar un índice FULLTEXT en su lugar.
Columna possible_keys
Esta columna muestra la lista de índices que MySQL consideró como candidatos para resolver la consulta. MySQL analiza las condiciones del WHERE, las cláusulas JOIN y las expresiones ORDER BY/GROUP BY para identificar qué índices podrían ser relevantes.
Un valor NULL significa que MySQL no encontró ningún índice aplicable, lo que generalmente desemboca en un full table scan. Cuando ves varios índices listados separados por comas, significa que hay múltiples candidatos y MySQL elegirá el que estime más eficiente.
EXPLAIN SELECT * FROM pedidos
WHERE cliente_id = 42 AND estado = 'pendiente';| id | select_type | table | type | possible_keys | key | rows |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | ref | idx_cliente_id,idx_estado | idx_cliente_id | 8 |
MySQL consideró dos índices (idx_cliente_id e idx_estado) pero eligió idx_cliente_id porque estima que filtra mejor. Si la combinación de ambas columnas se consulta frecuentemente, un índice compuesto (cliente_id, estado) sería más eficiente que dos índices simples.
Columna key
Mientras que possible_keys muestra los candidatos, key muestra el índice que MySQL realmente decidió usar. Esta es la columna que confirma qué índice está en acción. Si el valor es NULL, MySQL no usó ningún índice y la consulta probablemente hace un full table scan.
En ocasiones, el índice en key no aparece en possible_keys. Esto sucede cuando MySQL elige un covering index que cubre todas las columnas del SELECT, aunque técnicamente no sea útil para filtrar el WHERE. MySQL lo prefiere porque puede leer todos los datos directamente del índice sin acceder a la tabla.
Si necesitas forzar a MySQL a usar un índice específico (o ignorar uno que elige mal), puedes usar las directivas FORCE INDEX o IGNORE INDEX. Pero en general, el optimizador de MySQL toma buenas decisiones si las estadísticas del índice están actualizadas.
Columna key_len
La longitud de clave indica cuántos bytes del índice utiliza MySQL realmente. Esta información es especialmente valiosa con índices compuestos, porque te permite saber cuántas columnas del índice participan en la búsqueda.
Cada tipo de dato contribuye un número fijo de bytes: un INT ocupa 4 bytes, un BIGINT ocupa 8 bytes, y un VARCHAR(50) con codificación utf8mb4 ocupa hasta 202 bytes (50 caracteres x 4 bytes + 2 bytes de longitud). Si la columna admite NULL, se añade 1 byte adicional.
-- Índice compuesto: (estado VARCHAR(20), fecha_pedido DATE)
-- estado: 20 * 4 + 2 = 82 bytes (utf8mb4)
-- fecha_pedido: 3 bytes
EXPLAIN SELECT * FROM pedidos
WHERE estado = 'pendiente' AND fecha_pedido > '2026-01-01';| id | select_type | table | type | key | key_len | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | range | idx_estado_fecha | 85 | 120 | Using index condition |
El key_len: 85 indica que MySQL usa las dos columnas del índice (82 bytes de estado + 3 bytes de fecha_pedido). Si el key_len fuera solo 82, sabríamos que solo se usa la primera columna y que la condición sobre fecha_pedido se evalúa después de la búsqueda en el índice.
Columna ref
La columna ref muestra qué valores se comparan con el índice elegido. Puede contener constantes literales, nombres de columnas de otras tablas o funciones.
Cuando ves const, significa que MySQL compara el índice contra un valor fijo proporcionado en el WHERE. Cuando ves una referencia como tienda.clientes.id, indica que el valor proviene de otra tabla en un JOIN y cambia fila a fila.
EXPLAIN SELECT c.nombre, p.fecha_pedido
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id
WHERE c.ciudad = 'Madrid';| id | select_type | table | type | key | ref | rows |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | c | ref | idx_ciudad | const | 45 |
| 1 | SIMPLE | p | ref | idx_cliente_id | tienda.c.id | 8 |
La primera fila usa const porque 'Madrid' es un valor literal. La segunda fila usa tienda.c.id porque el valor de cliente_id se toma dinámicamente de cada fila de la tabla clientes. Si ves func en esta columna, indica que MySQL aplica una función sobre el valor antes de compararlo con el índice.
Columna rows
Esta columna muestra la estimación de MySQL sobre cuántas filas necesita examinar para completar esa parte de la consulta. Es importante recordar que se trata de una estimación basada en las estadísticas del índice, no de un conteo exacto. En tablas donde las estadísticas no se han actualizado recientemente, esta cifra puede diferir significativamente de la realidad.
En un JOIN de varias tablas, el impacto se multiplica. Si la primera tabla estima 100 filas y la segunda estima 50 filas por cada una, MySQL examinará potencialmente 100 x 50 = 5.000 combinaciones. Por eso, reducir el número de filas estimadas en las tablas iniciales tiene un efecto cascada sobre toda la consulta.
EXPLAIN SELECT c.nombre, p.total
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id
WHERE c.ciudad = 'Valencia' AND p.total > 500;| id | select_type | table | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | c | ref | idx_ciudad | 28 | 100.00 | NULL |
| 1 | SIMPLE | p | ref | idx_cliente_id | 8 | 33.33 | Using where |
MySQL estima examinar 28 clientes de Valencia y, para cada uno, 8 pedidos. Pero solo un tercio de esos pedidos (33.33% de filtered) cumple la condición total > 500. El total efectivo es 28 x 8 x 0.33 = aproximadamente 74 filas. Si esta cifra te parece excesiva, puedes usar EXPLAIN ANALYZE para ver las filas reales examinadas durante la ejecución.
Columna filtered
La columna filtered expresa como porcentaje cuántas de las filas estimadas en rows sobrevivirán después de aplicar las condiciones del WHERE que no se resolvieron mediante el índice. Un valor de 100.00 indica que todas las filas estimadas cumplen las condiciones, es decir, el índice ya hizo todo el trabajo de filtrado. Un valor bajo como 10.00 significa que el 90% de las filas leídas se descartarán.
Esta columna es clave para evaluar la eficiencia real de una consulta. Si ves rows: 50000 y filtered: 1.00, MySQL está leyendo 50.000 filas para quedarse con solo 500. Eso indica que un índice mejor podría eliminar la mayoría de esas lecturas innecesarias.
EXPLAIN SELECT * FROM productos
WHERE categoria_id = 5 AND precio > 200 AND stock > 0;| id | select_type | table | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | productos | ref | idx_categoria | 30 | 11.11 | Using where |
MySQL usa el índice de categoría para encontrar 30 productos, pero luego descarta casi el 89% porque no cumplen las condiciones de precio y stock. Un índice compuesto (categoria_id, precio) reduciría las filas leídas y mejoraría el filtered.
Columna Extra
La columna Extra contiene información adicional que no encaja en las demás columnas pero que resulta fundamental para entender qué está haciendo MySQL internamente. Aquí aparecen los indicadores más reveladores sobre la calidad de la ejecución.
Using index
Este mensaje indica que MySQL puede resolver la consulta leyendo únicamente el índice, sin necesidad de acceder a la tabla. Esto se conoce como covering index y es una de las optimizaciones más potentes, porque los índices son más compactos y rápidos de leer que las filas completas de la tabla.
EXPLAIN SELECT cliente_id, fecha_pedido FROM pedidos
WHERE cliente_id = 42;| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | ref | idx_cliente_fecha | 8 | Using index |
Si el índice idx_cliente_fecha contiene las columnas (cliente_id, fecha_pedido), MySQL no necesita ir a la tabla para nada. Todas las columnas del SELECT y del WHERE están cubiertas por el índice. Para aprovechar esta optimización, diseña índices que incluyan las columnas que más consultas de forma conjunta.
Using where
Este mensaje aparece cuando MySQL necesita aplicar condiciones de filtrado adicionales después de leer las filas del índice (o de la tabla si no hay índice). Por sí solo no es malo, pero cuando aparece junto con type: ALL, indica que MySQL está leyendo toda la tabla y descartando filas que no cumplen el WHERE.
EXPLAIN SELECT * FROM pedidos
WHERE cliente_id = 42 AND observaciones LIKE '%urgente%';| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | ref | idx_cliente_id | 8 | Using where |
MySQL usa el índice para encontrar los pedidos del cliente 42, pero luego aplica el filtro LIKE sobre observaciones fila a fila porque esa columna no está en el índice.
Using temporary
Este indicador señala que MySQL necesita crear una tabla temporal interna para procesar la consulta. Aparece con frecuencia en consultas que combinan GROUP BY y ORDER BY sobre columnas distintas, en UNION, y en ciertos tipos de subconsultas.
EXPLAIN SELECT ciudad, COUNT(*) AS total
FROM clientes
GROUP BY ciudad
ORDER BY total DESC;| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | clientes | ALL | NULL | 200 | Using temporary; Using filesort |
Las tablas temporales no son siempre un problema, pero en consultas que procesan muchas filas pueden degradar significativamente el rendimiento, especialmente si la tabla temporal excede el tamaño de memoria y se escribe a disco.
Using filesort
A pesar de su nombre, Using filesort no implica necesariamente que MySQL escriba en disco. Indica que MySQL necesita un paso de ordenación adicional porque no puede obtener las filas en el orden requerido directamente desde un índice. En tablas pequeñas es rápido, pero en tablas grandes con millones de filas, el coste puede ser elevado.
EXPLAIN SELECT * FROM productos ORDER BY precio DESC;Si no existe un índice sobre precio, MySQL leerá todos los productos y los ordenará en memoria (o en disco si no caben). Crear un índice sobre precio eliminaría el filesort por completo.
Using index condition
Este mensaje indica que MySQL usa Index Condition Pushdown (ICP), una optimización donde parte de la condición WHERE se evalúa directamente en el motor de almacenamiento mientras recorre el índice, antes de leer la fila completa de la tabla. Esto reduce las lecturas a la tabla y mejora el rendimiento.
EXPLAIN SELECT * FROM pedidos
WHERE estado = 'pendiente' AND fecha_pedido > '2026-03-01';| id | select_type | table | type | key | key_len | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | pedidos | range | idx_estado_fecha | 85 | 45 | Using index condition |
Con un índice compuesto (estado, fecha_pedido), MySQL usa la primera columna para filtrar por estado y luego aplica la condición de rango sobre la fecha directamente en el índice, sin leer filas que no cumplan ambas condiciones.
Otros valores de Extra
Existen más mensajes que puedes encontrar en la columna Extra. Using join buffer indica que MySQL usa un buffer en memoria para almacenar filas de la tabla anterior en un JOIN, lo que ocurre cuando no hay un índice adecuado en la tabla unida. Impossible WHERE significa que MySQL detectó durante la planificación que la condición WHERE es imposible de satisfacer, así que no ejecuta la consulta. Using MRR indica el uso de Multi-Range Read, una optimización que agrupa lecturas de disco para reducir accesos aleatorios. Select tables optimized away aparece cuando MySQL resuelve la consulta durante la planificación, normalmente en consultas con MIN() o MAX() sobre columnas indexadas.
Cheat sheet de patrones comunes
Después de analizar cientos de planes de ejecución, ciertos patrones aparecen una y otra vez. Esta referencia rápida te ayudará a identificar los escenarios más frecuentes y a tomar decisiones de optimización sin tener que reanalizar cada columna.
Consulta ideal: acceso por clave primaria
El mejor plan posible es aquel donde MySQL accede directamente a una fila mediante la clave primaria o un índice único.
EXPLAIN SELECT * FROM clientes WHERE id = 100;
-- type: const | key: PRIMARY | rows: 1Cuando ves type: const y rows: 1, no hay nada que optimizar. MySQL llega a la fila en tiempo constante.
JOIN eficiente entre dos tablas
El patrón ideal para un JOIN es que la tabla guía se filtre bien y la tabla unida use eq_ref o ref.
EXPLAIN SELECT c.nombre, p.total
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id
WHERE c.email = 'ana@ejemplo.com';
-- clientes: type: const (índice único en email)
-- pedidos: type: ref (índice en cliente_id)Si la tabla de pedidos muestra type: ALL en lugar de ref, falta un índice sobre la columna del JOIN.
Full table scan: la señal de alerta
Cuando type: ALL aparece con un valor alto en rows, es la señal más clara de que falta un índice.
EXPLAIN SELECT * FROM pedidos WHERE fecha_pedido > '2026-01-01';
-- type: ALL | key: NULL | rows: 50000La solución habitual es crear un índice sobre la columna del WHERE. Después del índice, esperas ver type: range con un rows mucho menor.
Filesort y temporary: optimizables con índices
Cuando ves Using temporary; Using filesort en una consulta GROUP BY, a menudo puedes eliminarlo con un índice que cubra las columnas del WHERE y del GROUP BY en el orden correcto.
-- Antes (sin índice):
-- type: ALL | Extra: Using temporary; Using filesort
-- Después de CREATE INDEX idx_estado_ciudad ON clientes (estado, ciudad):
-- type: ref | Extra: Using indexSubconsulta correlacionada costosa
Las subconsultas correlacionadas con select_type: DEPENDENT SUBQUERY se ejecutan una vez por cada fila de la consulta exterior. Si la consulta exterior devuelve muchas filas, el coste se dispara.
EXPLAIN SELECT nombre, (
SELECT MAX(total) FROM pedidos WHERE cliente_id = clientes.id
) FROM clientes;
-- select_type: DEPENDENT SUBQUERY | rows: 8 (por cada cliente)La alternativa es convertir la subconsulta en un JOIN con GROUP BY, lo que permite a MySQL ejecutar una sola pasada sobre la tabla de pedidos.
Covering index: la optimización silenciosa
Cuando ves Extra: Using index sin Using where, MySQL está leyendo todo directamente del índice. Este es el patrón más eficiente para consultas que solo necesitan columnas incluidas en el índice.
EXPLAIN SELECT cliente_id, COUNT(*) FROM pedidos GROUP BY cliente_id;
-- type: index | key: idx_cliente_id | Extra: Using indexSi necesitas columnas adicionales que no están en el índice, el Using index desaparecerá y MySQL tendrá que acceder a la tabla.
Resumen de tipos de acceso
Para tener siempre presente la jerarquía de rendimiento, esta es la escala de tipos de acceso ordenada de mejor a peor: system y const representan el acceso instantáneo a una fila conocida. eq_ref es el JOIN perfecto con una fila por búsqueda. ref busca por índice no único, rápido pero con múltiples filas. range recorre un rango del índice y su eficiencia depende de la amplitud. index recorre el índice completo, mejor que la tabla completa pero sigue siendo un scan. ALL es el full table scan, la opción más lenta que debería evitarse en tablas grandes.
Cuando analices un EXPLAIN, empieza siempre por la columna type. Si alguna fila muestra ALL sobre una tabla grande, ese es tu primer objetivo de optimización. Después revisa Extra para detectar tablas temporales y filesorts innecesarios. Finalmente, usa rows y filtered para estimar el volumen real de trabajo y decidir si un índice adicional merece la pena.
Escrito por Eduardo Lázaro
