🐬 Curso SQL · Nivel N4 Intermedio

Bloque 4: Agregar y resumir datos

60 ventas dejan de ser aburridas en cuanto las resumes: totales, medias, máximos, agrupaciones por región o vendedor. Dominarás COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING, las subconsultas y los bloques reutilizables con CTEs (WITH).

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

Funciones de agregación

Las funciones de agregación toman muchas filas y devuelven un solo valor-resumen. Son el corazón de los informes.

1.1 · Las cinco funciones básicas

el informe completo en una fila
SELECT COUNT(*)     AS num_ventas,
       SUM(cantidad) AS unidades,
       SUM(cantidad * precio) AS importe_total,
       AVG(precio)   AS precio_medio,
       MIN(precio)   AS precio_minimo,
       MAX(precio)   AS precio_maximo
FROM   ventas;
FunciónQué devuelve
COUNT(*)Número de filas del grupo
SUM(col)Suma de los valores de la columna
AVG(col)Media aritmética
MIN(col) / MAX(col)Valor mínimo / máximo
⚠️ AVG, SUM… ignoran los NULL. COUNT(*) cuenta filas (aunque todo sea NULL); COUNT(col) cuenta solo las que no son NULL.

1.2 · Columnas calculadas en agregados

Puedes agregar una expresión: la venta real de cada fila no es precio, sino cantidad * precio. Suma eso:

ingresos totales
SELECT SUM(cantidad * precio) AS ingresos
FROM   ventas;
💡 Los alias con AS son imprescindibles aquí: sin ellos, la columna se llamaría SUM(cantidad * precio), poco legible para tu informe.
🎉 Ya resumen filas. Ahora a segmentar el resumen con GROUP BY.
Lección 2 · Teoría

GROUP BY

GROUP BY parte las filas en grupos (uno por valor distinto de la columna) y aplica la agregación a cada grupo. De una tabla de 60 ventas saca un resumen por región.

2.1 · Un resumen por cada grupo

ventas e ingresos por región
SELECT region,
       COUNT(*) AS ventas,
       SUM(cantidad * precio) AS ingresos
FROM   ventas
GROUP BY region;

Regla de oro: toda columna del SELECT que no sea una agregación tiene que estar en el GROUP BY. Si escribes SELECT region, vendedor, SUM(...) con GROUP BY region, el motor no sabe qué vendedor mostrar por grupo.

⚠️ En MySQL con ONLY_FULL_GROUP_BY activado (por defecto desde MySQL 5.7) esa consulta falla con error de sintaxis. Acostúmbrate a la regla desde ya.

2.2 · Agrupar por varias columnas

Varios niveles de detalle en el mismo resumen:

ingresos por región y producto
SELECT region, producto,
       SUM(cantidad * precio) AS ingresos
FROM   ventas
GROUP BY region, producto;
💡 Acompaña el GROUP BY de un ORDER BY para que el informe salga ordenado: GROUP BY region ORDER BY ingresos DESC.

2.3 · GROUP BY y JOIN

Funcionan juntos sin problema: agrupa después de unir tablas (lo viste con CASE en el bloque 2):

resumen por producto
SELECT producto,
       COUNT(*) AS lineas,
       AVG(precio) AS precio_medio
FROM   ventas
GROUP BY producto
ORDER BY lineas DESC;
🎉 Agrupas y ordenas resúmenes. Ahora filtra después de agrupar: HAVING.
Lección 3 · Teoría

HAVING

WHERE filtra filas antes de agrupar; HAVING filtra grupos después de agrupar. No son intercambiables: no puedes poner una agregación en el WHERE.

regiones con más de 12 ventas
SELECT region,
       COUNT(*) AS ventas
FROM   ventas
GROUP BY region
HAVING COUNT(*) > 12;

Y el clásico «solo los vendedores que superan la media» (sin HAVING, imagina intentarlo con WHERE):

vendedores con más de 25.000 € en ingresos
SELECT vendedor,
       SUM(cantidad * precio) AS ingresos
FROM   ventas
GROUP BY vendedor
HAVING SUM(cantidad * precio) > 25000
ORDER BY ingresos DESC;
⚠️ Orden lógico de ejecución: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Por eso no puedes usar alias del SELECT en el WHERE, pero sí en el ORDER BY y a veces en el HAVING (según motor).

3.1 · WHERE y HAVING juntos

filtro de filas + filtro de grupos
SELECT vendedor,
       COUNT(*) AS ventas_audio
FROM   ventas
WHERE  categoria = 'Audio'
GROUP BY vendedor
HAVING COUNT(*) >= 3;
💡 «El WHERE va antes del GROUP BY, el HAVING después». Si te sale error al poner un agregado en WHERE, es que querías usar HAVING.
🎉 Filtras grupos. Ahora una consulta dentro de otra consulta: las subconsultas.
Lección 4 · Teoría

Subconsultas

Una subconsulta es un SELECT dentro de otro. Sirve para usar «el resultado de una consulta» como parte de otra: en el WHERE, en el FROM o en el SELECT.

4.1 · Subconsulta escalar en WHERE

Devuelve una sola fila con un solo valor (un número, por ejemplo):

ventas cuyo precio supera la media global
SELECT producto, precio
FROM   ventas
WHERE  precio > (SELECT AVG(precio) FROM ventas);

Se calcula primero la subconsulta (la media) y luego se usa como valor de comparación. Nótese: la subconsulta va entre paréntesis.

el producto más caro de la tabla
SELECT producto, precio
FROM   ventas
WHERE  precio = (SELECT MAX(precio) FROM ventas);

4.2 · Subconsulta con IN

Cuando la subconsulta devuelve varias filas, se usa IN:

regiones que venden Audio
SELECT DISTINCT region
FROM   ventas
WHERE  region IN (
    SELECT DISTINCT region
    FROM   ventas
    WHERE  categoria = 'Audio'
);

4.3 · Subconsulta en el FROM

El resultado de la subconsulta se trata como una tabla derivada, con su alias. Así consultas «sobre un resumen»:

resumen de vendedores y media del resumen
SELECT AVG(ingresos) AS media_por_vendedor
FROM (
    SELECT vendedor, SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY vendedor
) AS resumen;
⚠️ Una subconsulta en el FROM necesita alias: ) AS resumen. Sin él, MySQL da error.
💡 Las subconsultas en el WHERE pierden legibilidad cuando se anidan mucho. Cuando eso pase, pasa a los CTEs: son la misma idea pero mucho más limpia.
🎉 Anidas consultas. Y ahora, la versión limpia y reutilizable: CTE con WITH.
Lección 5 · Teoría

CTEs con WITH

Un CTE (Common Table Expression) es un bloque con nombre definido con WITH que puedes usar en el resto de la consulta como si fuera una tabla temporal en memoria. Es la forma moderna y legible de encadenar pasos.

5.1 · Primer CTE

definir ingresos_por_vendedor y leerlo
WITH ingresos_por_vendedor AS (
    SELECT vendedor, SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY vendedor
)
SELECT * FROM ingresos_por_vendedor
ORDER BY ingresos DESC;

Define el bloque ingresos_por_vendedor y, justo después, la consulta principal lo consulta. Multiplicar esto por el resto es la gracia: reutilizas el paso.

5.2 · Varios CTEs y uso múltiple

dos CTEs: total por región y su máximo
WITH ingresos_por_region AS (
    SELECT region, SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY region
),
top_region AS (
    SELECT MAX(ingresos) AS mejor FROM ingresos_por_region
)
SELECT region, ingresos
FROM   ingresos_por_region
WHERE  ingresos = (SELECT mejor FROM top_region);
⚠️ Un CTE define un único bloque para la consulta siguiente. Se separan con coma, y el CTE top_region puede usar a ingresos_por_region. El WITH solo aparece una vez, al principio.

5.3 · CTE + JOIN

El combo definitivo: preparar un resumen y cruzar con combos:

porcentaje respecto al total general
WITH por_region AS (
    SELECT region, SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY region
)
SELECT region, ingresos,
       ROUND(ingresos * 100 /
       (SELECT SUM(ingresos) FROM por_region), 1) AS pct
FROM   por_region
ORDER BY ingresos DESC;
💡 Cuando una consulta tiene más de ~15 líneas, divídela en CTEs: cada paso con nombre y probado por separado. Es mucho más fácil de depurar.
🎉 Bloque 4 completado: resumes, agrupas, filtras grupos, anidas y encadenas con CTEs. Ahora machaca los 15 ejercicios con el histórico de ventas. 🐬
Manos a la obra

✍️ Los 15 ejercicios del bloque

Abre MySQL Workbench, conecta y ejecuta sobre la base tw_ventas. El objetivo: sacar informes reales de la tabla ventas. 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-bloque4_ventas.sql   y cárgala con mysql -u root -p < sql/sql-bloque4_ventas.sql.

Sección A · Agregación

1 Cuántas ventas hay

Muestra el número total de ventas con COUNT(*) y el alias num_ventas.

Ver solución
-- Ejercicio 1: total de ventas
SELECT COUNT(*) AS num_ventas
FROM   ventas;

2 Unidades e importe totales

Muestra el total de unidades vendidas (SUM(cantidad)) y el importe total (SUM(cantidad * precio)), con sus alias.

Ver solución
-- Ejercicio 2: unidades e importe
SELECT SUM(cantidad)        AS unidades,
       SUM(cantidad * precio) AS importe_total
FROM   ventas;

3 Precios extremos

Con MIN y MAX: el precio más barato precio_minimo y el más caro precio_maximo.

Ver solución
-- Ejercicio 3: precios extremos
SELECT MIN(precio) AS precio_minimo,
       MAX(precio) AS precio_maximo
FROM   ventas;

4 Ticket medio

Calcula el importe medio por venta con AVG(cantidad * precio) redondeado a 2 decimales, alias ticket_medio.

Ver solución
-- Ejercicio 4: ticket medio por venta
SELECT ROUND(AVG(cantidad * precio), 2) AS ticket_medio
FROM   ventas;

Sección B · GROUP BY

5 Ventas por región

Resumen: region y COUNT(*) como ventas, agrupado por región.

Ver solución
-- Ejercicio 5: ventas por región
SELECT region,
       COUNT(*) AS ventas
FROM   ventas
GROUP BY region;

6 Ingresos por región

Agrupación por región con SUM(cantidad * precio) como ingresos, ordenado de mayor a menor ingresos.

Ver solución
-- Ejercicio 6: ingresos por región
SELECT region,
       SUM(cantidad * precio) AS ingresos
FROM   ventas
GROUP BY region
ORDER BY ingresos DESC;

7 Batería por vendedor

Por vendedor: COUNT(*) como ventas y SUM(cantidad) como unidades, ordenado por ventas descendente.

Ver solución
-- Ejercicio 7: rendimiento por vendedor
SELECT vendedor,
       COUNT(*) AS ventas,
       SUM(cantidad) AS unidades
FROM   ventas
GROUP BY vendedor
ORDER BY ventas DESC;

8 Precio medio por categoría

Por categoria: COUNT(*) como productos_distintos y AVG(precio) como precio_medio, redondeado a 2 decimales.

Ver solución
-- Ejercicio 8: media por categoría
SELECT categoria,
       COUNT(*) AS ventas,
       ROUND(AVG(precio), 2) AS precio_medio
FROM   ventas
GROUP BY categoria;

Sección C · HAVING

9 Regiones con más de 12 ventas

Región con COUNT(*) como ventas, agrupado, quedándote solo con las que tengan más de 12.

Ver solución
-- Ejercicio 9: regiones activas
SELECT region,
       COUNT(*) AS ventas
FROM   ventas
GROUP BY region
HAVING COUNT(*) > 12;

10 Vendedores top

Vendedores con SUM(cantidad * precio) como ingresos, con HAVING para quedarse con los que superen 25.000 €, ordenado por ingresos descendente.

Ver solución
-- Ejercicio 10: los grandes vendedores
SELECT vendedor,
       SUM(cantidad * precio) AS ingresos
FROM   ventas
GROUP BY vendedor
HAVING SUM(cantidad * precio) > 25000
ORDER BY ingresos DESC;

Sección D · Subconsultas

11 Por encima de la media

Ventas donde el precio sea mayor que la media global, usando una subconsulta con AVG en el WHERE. Muestra producto y precio.

Ver solución
-- Ejercicio 11: precios por encima de la media
SELECT producto, precio
FROM   ventas
WHERE  precio > (SELECT AVG(precio) FROM ventas);

12 El producto más caro

La venta con el precio máximo: usa WHERE precio = (SELECT MAX(precio)…), muestra producto y precio.

Ver solución
-- Ejercicio 12: el más caro
SELECT producto, precio
FROM   ventas
WHERE  precio = (SELECT MAX(precio) FROM ventas);

13 Regiones con ventas de Audio

Con subconsulta + IN: qué region ha vendido la categoría 'Audio'. Usa DISTINCT fuera y dentro.

Ver solución
-- Ejercicio 13: regiones con audio
SELECT DISTINCT region
FROM   ventas
WHERE  region IN (
    SELECT DISTINCT region
    FROM   ventas
    WHERE  categoria = 'Audio'
);

Sección E · CTE

14 CTE de ingresos por vendedor

Con WITH ingresos_por_vendedor AS (…) define un bloque que agrube por vendedor. Luego selecciona y ordena por ingresos descendente.

Ver solución
-- Ejercicio 14: CTE básico
WITH ingresos_por_vendedor AS (
    SELECT vendedor, SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY vendedor
)
SELECT * FROM ingresos_por_vendedor
ORDER BY ingresos DESC;

15 Dos CTEs: la mejor región

Crea el CTE ingresos_por_region (region, ingresos) y un segundo CTE top_region con el mejor importe. Consulta la región cuyo ingreso sea igual a ese mejor importe.

Ver solución
-- Ejercicio 15: mejor región con CTEs
WITH ingresos_por_region AS (
    SELECT region, SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY region
),
top_region AS (
    SELECT MAX(ingresos) AS mejor FROM ingresos_por_region
)
SELECT region, ingresos
FROM   ingresos_por_region
WHERE  ingresos = (SELECT mejor FROM top_region);

16 🏆 Proyecto integrador: informe de ventas por región

Crea el fichero informe_regiones.sql con una sola consulta usando CTE que devuelva un informe profesional del histórico de ventas. Requisitos:

  • Un CTE por_region que agrube por region con COUNT(*) como ventas y SUM(cantidad * precio) como ingresos.
  • Un CTE total_general con el SUM(ingresos) global.
  • Consulta final que cruce ambos con JOIN y calcule pct = ingresos · 100 / total, con ROUND a 1 decimal.
  • Para el filtro de agrupaciones usa HAVING (o WHERE si filtras antes de agrupar) según lo que consideres; mínimo para entregar: ORDER BY ingresos DESC.
Ver una solución posible
-- Informe: ventas por región y su peso sobre el total
WITH por_region AS (
    SELECT region,
           COUNT(*) AS ventas,
           SUM(cantidad * precio) AS ingresos
    FROM   ventas
    GROUP BY region
),
total_general AS (
    SELECT SUM(ingresos) AS total FROM por_region
)
SELECT r.region,
       r.ventas,
       r.ingresos,
       ROUND(r.ingresos * 100 / t.total, 1) AS pct
FROM   por_region r
CROSS JOIN total_general t
ORDER BY r.ingresos DESC;
Evaluación del bloque

📝 Examen del Bloque 4

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é devuelve COUNT(*)?
2 · ¿Qué función calcula la media?
3 · Toda columna del SELECT que no sea agregación debe estar…
4 · ¿Qué cláusula filtra los GRUPOS después de agrupar?
5 · ¿Se puede usar SUM() en el WHERE?
6 · ¿Qué operador compara con el resultado de una subconsulta de un solo valor?
7 · Cuando una subconsulta devuelve varias filas, se usa…
8 · Una subconsulta en el FROM necesita…
9 · CON qué palabra empieza un CTE?
10 · ¿Se puede usar un CTE en otra consulta?
11 · ¿Cómo se separan dos CTEs seguidos?
12 · ¿Cuál es el orden de ejecución correcto?
13 · MIN y MAX se usan con…
14 · ¿Se pueden usar agregados con JOIN?
15 · ¿Qué hace COUNT(columna) con los NULL?

Parte B · Reto «informe de regiones»

Crea informe_regiones.sql: un informe con CTE que muestre región, número de ventas, ingresos y porcentaje sobre el total, ordenado por ingresos 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: ya resumes y analizas datos como un analista junior. En el bloque 5 cambiarás de papel: diseñar y normalizar bases de datos como arquitecto, y crear vistas. 🐬🔥