🐬 Curso SQL · Nivel N7 Experto

Bloque 7: Rendimiento y ventanas

Llega el momento de volar: aprende a leer planes de ejecución con EXPLAIN, a diseñar índices que hacen volar las consultas, a optimizar lo que escribes y a usar las funciones de ventana (ROW_NUMBER, RANK, PARTITION BY, totales acumulados) para análisis que con GROUP BY eran imposibles en una sola consulta.

⏱ ≈ 17 horas 📚 5 lecciones ✍️ 15 ejercicios guiados 📝 Examen del bloque + laboratorio
Lección 1 · Teoría

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.

el plan de una consulta simple
EXPLAIN SELECT * FROM pedidos
WHERE cliente_id = 3;

Las columnas que más te dirán, de izquierda a derecha:

ColumnaQué te cuenta
typeCómo se accede: ALL (escaneo completo), index, range, ref, const
keyEl índice que se va a usar (o NULL si ninguno)
rowsFilas que estima revisar (cuanto menos, mejor)
possible_keysÍndices candidatos
⚠️ `ALL` con `rows` altísimas = escaneo de toda la tabla. No siempre es un error (tablas pequeñas), pero en producción con millones de filas suele ser el motivo del «lentón».

1.1 · type de más lento a más rápido

ALL — recorre todas las filas (el peor posible)
index — recorre el índice entero (mejor que ALL)
range — solo un rango del índice (BETWEEN, >)
ref — valores exactos vía índice (cliente_id = 3)
const — valor único por clave primaria (la mejor)
💡 La disciplina es siempre la misma: EXPLAIN la consulta lenta → detectas `ALL` o índices sin usar → decides el índice → vuelves a EXPLAIN → `rows` debería caer en picado.
🎉 Ya miras bajo el capó. Ahora la herramienta estrella para que el plan sea bueno: los índices.
Lección 2 · Teoría

Í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

crear, ver y borrar
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:

esta consulta usa el compuesto
SELECT id, fecha
FROM   pedidos
WHERE  cliente_id = 2
ORDER BY fecha;
⚠️ El orden importa: en un índice (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

Columnas con pocos valores distintos (un estado con 3 valores) → poca ganancia
Tablas minúsculas → el índice cuesta más de lo que ahorra
Tablas con muchísimos INSERT/UPDATE → cada índice frena la escritura
TEXT largos → usa prefijo: CREATE INDEX … ON productos (nombre(30))
💡 Las claves primarias y foráneas ya crean índices por sí solas. No dupliques el trabajo creando otro índice igual.
🎉 Índices al cinto. Ahora saca el máximo de lo que ya escribes: optimización.
Lección 3 · Teoría

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

mata el índice con una función sobre la columna
SELECT * FROM pedidos
WHERE DATE_FORMAT(fecha, '%Y-%m') = '2026-03';
mismo objetivo, índice usable (sargable)
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 necesitas
Sin LIMIT, una consulta grande puede traer 100.000 filas: paga por página
Con ORDER BY columna_indexada el motor ordena sin ordenar: gratis
Evita SELECT * en un JOIN grande: menos tráfico y menos memoria

3.3 · El combo correcto

consulta optimizada de principio a fin
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
💡 Orden de trabajo: primero consulta limpia y sargable, después EXPLAIN, después el índice que falte. Optimizar de oído, sin EXPLAIN, es jugar a adivinar.
🎉 Todo vuela. Ahora el análisis fino que GROUP BY no puede: funciones de ventana.
Lección 4 · Teoría

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

cada pedido con su puesto por fecha
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

coste de envío por pedido, con 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;
RANK — empates: 1, 1, 3 (deja huecos)
DENSE_RANK — empates: 1, 1, 2 (sin huecos)
ROW_NUMBER — nunca repite: 1, 2, 3

4.3 · PARTITION BY: ventanas por grupo

Divide el resultado en grupos y rankea dentro de cada grupo. El top por cliente:

puesto dentro de cada cliente
SELECT cliente_id, id, fecha,
       ROW_NUMBER() OVER (
         PARTITION BY cliente_id
         ORDER BY fecha DESC
       ) AS pedido_actual
FROM   pedidos;
⚠️ «Over flight»: el 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.
🎉 Rankings por grupo. Ahora los agregados sin perder filas: totales, medias y acumulados con OVER.
Lección 5 · Teoría

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

el precio medio de su categoría, al lado de cada producto
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):

costes acumulados de envío en el tiempo
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:

mes anterior y diferencia (con fecha)
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;
💡 Ventana con GROUP BY lógico: quieres el top 1 de cada grupo → consulta con ROW_NUMBER + PARTITION BY dentro de un CTE, y fuera filtra la posición 1. Es el patrón «top de cada grupo».
🎉 Bloque 7 completado: lees planes, indexas a conciencia, escribes consultas limpias y analizas con ventanas. Ahora machaca los 15 ejercicios con el ERP. 🐬
Manos a la obra

✍️ 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.

📥 ¿No tienes la base de datos? Descárgala: sql/sql-bloque7_erp_pedidos.sql   y cárgala con 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 pedidos con empleados (alias) para tener el nombre del vendedor.
  • Añade un conteo por vendedor con COUNT(*) OVER (PARTITION BY vendedor_id) como ventas_vendedor.
  • Añade también ROW_NUMBER() OVER (ORDER BY fecha) como posicion_global.
  • Filtra con WHERE a partir de '2026-02-01'.
  • Ordena el resultado final con ORDER BY fecha DESC.
  • Incluye un EXPLAIN de 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;
Evaluación del bloque

📝 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 A · Test teórico (aprueba con 11/15)

1 · ¿Qué muestra EXPLAIN?
2 · En EXPLAIN, ¿qué type es el peor?
3 · ¿Para qué sirve un índice?
4 · En un índice compuesto (a, b)…
5 · ¿Qué condición es «sargable» (usa índice)?
6 · ¿Cuándo NO merece la pena un índice?
7 · ¿Qué función de ventana reparte 1, 2, 3… sin repetir?
8 · Con empates, RANK devuelve…
9 · ¿Qué hace PARTITION BY en una ventana?
10 · El ORDER BY DENTRO del OVER…
11 · ¿Qué genera SUM(x) OVER (ORDER BY id)?
12 · ¿Qué función compara con la fila ANTERIOR?
13 · A diferencia de GROUP BY, las funciones de ventana…
14 · Para el patrón «top 1 de cada grupo» se usa…
15 · ¿Qué restricción «compra precio de velocidad» al añadir índices de más?

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

🎉 ¿Superaste las dos partes? Enhorabuena: consultas rápidas y análisis de nivel experto. En el bloque 8 (el último) gobernarás el servidor: usuarios, permisos, backups y el proyecto final. 🐬🔥