EXPLAIN: leer el plan
Antes de preguntar «¿por qué va lenta?», pregúntale al motor qué plan ha elegido: EXPLAIN muestra cómo ejecutaría la consulta sin ejecutarla. Ahí se ve si usa índices o si escanea tabla entera.
EXPLAIN SELECT * FROM pedidos
WHERE cliente_id = 3;
Las columnas que más te dirán, de izquierda a derecha:
| Columna | Qué te cuenta |
|---|---|
type | Cómo se accede: ALL (escaneo completo), index, range, ref, const |
key | El índice que se va a usar (o NULL si ninguno) |
rows | Filas que estima revisar (cuanto menos, mejor) |
possible_keys | Índices candidatos |
1.1 · type de más lento a más rápido
Índices
Un índice es una estructura aparte que ordena los valores de una o varias columnas para encontrarlos sin recorrer la tabla. Es como el índice de un libro: no copia el libro, pero te lleva al punto exacto.
2.1 · Crear y borrar índices
CREATE INDEX idx_pedidos_fecha ON pedidos (fecha);
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id, fecha);
SHOW INDEX FROM pedidos;
DROP INDEX idx_pedidos_fecha ON pedidos;
2.2 · Índice compuesto
Las columnas se ordenan por prioridad. El índice (cliente_id, fecha) sirve para filtrar por
cliente y ordenar por fecha a la vez:
SELECT id, fecha
FROM pedidos
WHERE cliente_id = 2
ORDER BY fecha;
(a, b), la columna a es la
primera. Filtrar solo por b no aprovecha el índice. Diseña el orden según tus WHERE más
habituales.2.3 · Cuándo NO indexar
CREATE INDEX … ON productos (nombre(30))Optimizar consultas
Antes de tocar un índice, revisa la consulta: muchas «lentas» son el resultado de cómo está escrita.
3.1 · No «escondas» las columnas
SELECT * FROM pedidos
WHERE DATE_FORMAT(fecha, '%Y-%m') = '2026-03';
SELECT * FROM pedidos
WHERE fecha >= '2026-03-01'
AND fecha < '2026-04-01';
Envolver la columna en una función (DATE_FORMAT, UPPER, LENGTH…) impide usar el índice. La regla sargable: deja la columna limpia y mete las funciones en el valor.
WHERE UPPER(nombre) = 'ACME' hace que el índice de nombre no sirva.
Mejor guarda el dato normalizado o compara el valor normalizado.3.2 · Pide solo lo que necesitas
SELECT * trae columnas que quizá ni usas: pide solo las que necesitasLIMIT, una consulta grande puede traer 100.000 filas: paga por páginaORDER BY columna_indexada el motor ordena sin ordenar: gratisSELECT * en un JOIN grande: menos tráfico y menos memoria3.3 · El combo correcto
SELECT p.id, p.fecha, c.nombre AS cliente
FROM pedidos p
INNER JOIN clientes c ON c.id = p.cliente_id
WHERE p.fecha >= '2026-03-01'
AND p.fecha < '2026-04-01'
ORDER BY p.fecha;
-- plan: EXPLAIN delante y revisa key y rows
Ventanas I: ranking
Las funciones de ventana calculan sobre un conjunto de filas sin colapsarlas
(a diferencia de GROUP BY): cada fila del resultado sobrevive y gana una columna extra calculada. Se escriben
con OVER().
4.1 · ROW_NUMBER: posiciones puras
SELECT id, vendedor_id, fecha,
ROW_NUMBER() OVER (ORDER BY fecha) AS posicion
FROM pedidos;
ROW_NUMBER() reparte números correlativos 1, 2, 3… sin repetir. ORDER BY del
OVER decide el orden del conteo.
4.2 · RANK y DENSE_RANK: los empates
SELECT id, ciudad, coste,
RANK() OVER (ORDER BY coste DESC) AS rango,
DENSE_RANK() OVER (ORDER BY coste DESC) AS rango_denso
FROM envios;
4.3 · PARTITION BY: ventanas por grupo
Divide el resultado en grupos y rankea dentro de cada grupo. El top por cliente:
SELECT cliente_id, id, fecha,
ROW_NUMBER() OVER (
PARTITION BY cliente_id
ORDER BY fecha DESC
) AS pedido_actual
FROM pedidos;
ORDER BY que va DENTRO del OVER sirve al ranking; el
ORDER BY de FUERA ordena el resultado final. Confundirlos es el error clásico.Ventanas II: agregados sobre la marcha
Las mismas funciones de agregación (SUM, AVG, COUNT, MIN, MAX) funcionan como ventana: devuelven el valor agregado de un grupo en cada fila del grupo, sin colapsar nada.
5.1 · Agregado con PARTITION BY
SELECT nombre, categoria_id, precio,
AVG(precio) OVER (PARTITION BY categoria_id) AS media_categoria
FROM productos;
5.2 · Acumulado con ORDER BY dentro de la ventana
Si el OVER lleva ORDER BY, la ventana se vuelve acumulativa: suma hasta esa
fila. Es un total corriente (running total):
SELECT id, ciudad, coste,
SUM(coste) OVER (
PARTITION BY ciudad
ORDER BY id
) AS acumulado_ciudad
FROM envios;
5.3 · LAG y LEAD: mirar hacia atrás y adelante
Comparan una fila con la anterior (LAG) o la siguiente (LEAD), ideal para
diferencias entre períodos:
SELECT id, fecha,
LAG(fecha) OVER (ORDER BY id) AS pedido_anterior,
DATEDIFF(fecha,
LAG(fecha) OVER (ORDER BY id)) AS dias_desde_anterior
FROM pedidos;
✍️ Los 15 ejercicios del bloque
Abre MySQL Workbench y ejecuta sobre la base tw_erp. Mira los planes con EXPLAIN y practica
las ventanas. Escribe las palabras clave en mayúsculas y añade un comentario -- con el número
del ejercicio.
mysql -u root -p < sql/sql-bloque7_erp_pedidos.sql.Sección A · EXPLAIN
1 Plan de una búsqueda por cliente
Mira el plan de SELECT * FROM pedidos WHERE cliente_id = 3 con EXPLAIN delante.
Ver solución
-- Ejercicio 1: plan de ejecución
EXPLAIN SELECT * FROM pedidos
WHERE cliente_id = 3;
2 Plan de un rango de fechas
Mira el plan de la consulta por rango de fechas de marzo, comparando antes y después de que exista el índice (que ya está creado en el dataset).
Ver solución
-- Ejercicio 2: rango de fechas
EXPLAIN SELECT id, fecha
FROM pedidos
WHERE fecha >= '2026-03-01'
AND fecha < '2026-04-01';
3 Listar los índices
Muestra todos los índices de la tabla pedidos con SHOW INDEX.
Ver solución
-- Ejercicio 3: índices de pedidos
SHOW INDEX FROM pedidos;
Sección B · Índices
4 Invertir el compuesto
Crea un índice idx_pedidos_cliente_fecha sobre pedidos con las columnas
(cliente_id, fecha).
Ver solución
-- Ejercicio 4: índice compuesto
CREATE INDEX idx_pedidos_cliente_fecha ON pedidos (cliente_id, fecha);
5 Índice en TEXT con prefijo
Crea sobre productos un índice idx_productos_nombre en nombre(30)
(con prefijo).
Ver solución
-- Ejercicio 5: índice con prefijo
CREATE INDEX idx_productos_nombre ON productos (nombre(30));
6 Borrar un índice
Borra el índice idx_pedidos_cliente_fecha que creaste en el ejercicio 4.
Ver solución
-- Ejercicio 6: retirar un índice
DROP INDEX idx_pedidos_cliente_fecha ON pedidos;
Sección C · Optimización
7 Consulta sargable
Escribe una consulta de pedidos de marzo 2026 que use el índice de fecha:
con fecha >= y fecha <, sin funciones sobre la columna.
Ver solución
-- Ejercicio 7: condición sargable
SELECT id, fecha
FROM pedidos
WHERE fecha >= '2026-03-01'
AND fecha < '2026-04-01';
8 Sin SELECT *
Consulta de pedidos con su cliente mostrando ELIJA solo las columnas id, fecha
y clientes.nombre (no use el asterisco).
Ver solución
-- Ejercicio 8: columnas concretas
SELECT p.id, p.fecha, c.nombre AS cliente
FROM pedidos p
INNER JOIN clientes c ON c.id = p.cliente_id;
9 Paginar con LIMIT
Los 5 pedidos más recientes, mostrando solo id y fecha, ordenados por fecha
descendente y con LIMIT 5.
Ver solución
-- Ejercicio 9: página pequeña
SELECT id, fecha
FROM pedidos
ORDER BY fecha DESC
LIMIT 5;
Sección D · Ranking con ventanas
10 Posición por fecha
Con ROW_NUMBER() OVER (ORDER BY fecha) da una posicion a cada pedido, mostrando
id, fecha y la posición.
Ver solución
-- Ejercicio 10: posiciones
SELECT id, fecha,
ROW_NUMBER() OVER (ORDER BY fecha) AS posicion
FROM pedidos;
11 Ranking por coste de envío
Con RANK() OVER (ORDER BY coste DESC) crea un rango para los envíos; muestra
id, ciudad, coste y rango.
Ver solución
-- Ejercicio 11: ranking con empates
SELECT id, ciudad, coste,
RANK() OVER (ORDER BY coste DESC) AS rango
FROM envios;
12 El último pedido de cada cliente
Con ROW_NUMBER() OVER (PARTITION BY cliente_id ORDER BY fecha DESC) y un CTE, muestra el
pedido más reciente de cada cliente.
Ver solución
-- Ejercicio 12: top 1 por cliente
WITH numerados AS (
SELECT cliente_id, id AS pedido, fecha,
ROW_NUMBER() OVER (
PARTITION BY cliente_id ORDER BY fecha DESC
) AS pos
FROM pedidos
)
SELECT cliente_id, pedido, fecha
FROM numerados
WHERE pos = 1;
Sección E · Agregados de ventana
13 Media por categoría
Muestra nombre, precio y la media_categoria con
AVG(precio) OVER (PARTITION BY categoria_id) para todos los productos.
Ver solución
-- Ejercicio 13: media dentro del grupo
SELECT nombre, precio,
AVG(precio) OVER (PARTITION BY categoria_id) AS media_categoria
FROM productos;
14 Costes acumulados por ciudad
Suma corriente: SUM(coste) OVER (PARTITION BY ciudad ORDER BY id) como
acumulado_ciudad, mostrando id, ciudad y coste.
Ver solución
-- Ejercicio 14: acumulado por ciudad
SELECT id, ciudad, coste,
SUM(coste) OVER (
PARTITION BY ciudad ORDER BY id
) AS acumulado_ciudad
FROM envios;
15 Días desde el pedido anterior
Con LAG(fecha) OVER (ORDER BY id) y DATEDIFF, calcula
dias_desde_anterior para cada pedido.
Ver solución
-- Ejercicio 15: LAG para comparar períodos
SELECT id, fecha,
LAG(fecha) OVER (ORDER BY id) AS pedido_anterior,
DATEDIFF(fecha,
LAG(fecha) OVER (ORDER BY id)) AS dias_desde_anterior
FROM pedidos;
16 🏆 Proyecto integrador: ranking de vendedores
Crea el fichero ranking_vendedores.sql con un análisis profesional de los pedidos.
Requisitos (una sola consulta, con CTE):
- Usa un CTE que junte
pedidosconempleados(alias) para tener elnombredel vendedor. - Añade un conteo por vendedor con
COUNT(*) OVER (PARTITION BY vendedor_id)comoventas_vendedor. - Añade también
ROW_NUMBER() OVER (ORDER BY fecha)comoposicion_global. - Filtra con
WHEREa partir de'2026-02-01'. - Ordena el resultado final con
ORDER BY fecha DESC. - Incluye un
EXPLAINde la consulta al inicio (opcional).
Ver una solución posible
-- Informe: ranking de vendedores con ventanas
EXPLAIN
WITH lista AS (
SELECT p.id, p.fecha, e.nombre AS vendedor,
COUNT(*) OVER (
PARTITION BY p.vendedor_id
) AS ventas_vendedor,
ROW_NUMBER() OVER (ORDER BY p.fecha) AS posicion_global
FROM pedidos p
INNER JOIN empleados e ON e.id = p.vendedor_id
WHERE p.fecha >= '2026-02-01'
)
SELECT id, vendedor, ventas_vendedor, posicion_global, fecha
FROM lista
ORDER BY fecha DESC;
📝 Examen del Bloque 7
Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con laboratorio.
Parte B · Reto «ranking de vendedores»
Crea ranking_vendedores.sql: consulta con CTE que junte pedidos y empleados, con el conteo
por vendedor (OVER PARTITION), posición global (ROW_NUMBER) y WHERE a partir de febrero, ordenada por fecha
descendente. Tu SQL no se ejecuta en el laboratorio: se valida con reglas automáticas. Mínimo para aprobar:
6 de 8 casillas.
Cumplidos: 0 / 8