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
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ón | Qué 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:
SELECT SUM(cantidad * precio) AS ingresos
FROM ventas;
SUM(cantidad * precio), poco legible para tu informe.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
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.
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:
SELECT region, producto,
SUM(cantidad * precio) AS ingresos
FROM ventas
GROUP BY region, producto;
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):
SELECT producto,
COUNT(*) AS lineas,
AVG(precio) AS precio_medio
FROM ventas
GROUP BY producto
ORDER BY lineas DESC;
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.
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):
SELECT vendedor,
SUM(cantidad * precio) AS ingresos
FROM ventas
GROUP BY vendedor
HAVING SUM(cantidad * precio) > 25000
ORDER BY ingresos DESC;
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
SELECT vendedor,
COUNT(*) AS ventas_audio
FROM ventas
WHERE categoria = 'Audio'
GROUP BY vendedor
HAVING COUNT(*) >= 3;
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):
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.
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:
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»:
SELECT AVG(ingresos) AS media_por_vendedor
FROM (
SELECT vendedor, SUM(cantidad * precio) AS ingresos
FROM ventas
GROUP BY vendedor
) AS resumen;
) AS resumen. Sin él, MySQL da error.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
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
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);
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:
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;
✍️ 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.
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_regionque agrube porregionconCOUNT(*)comoventasySUM(cantidad * precio)comoingresos. - Un CTE
total_generalcon elSUM(ingresos)global. - Consulta final que cruce ambos con JOIN y calcule
pct= ingresos · 100 / total, conROUNDa 1 decimal. - Para el filtro de agrupaciones usa
HAVING(oWHEREsi 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;
📝 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 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