GROUP BY agrupa las filas que tienen el mismo valor en una o varias columnas y aplica una función de agregado a cada grupo. Es la herramienta clave para generar resúmenes estadísticos: total de ventas por mes, número de pedidos por cliente, precio medio por categoría...
HAVING filtra esos grupos después de aplicar la agregación, igual que WHERE filtra filas antes. La distinción entre cuándo usar WHERE y cuándo usar HAVING es uno de los conceptos que más confusión genera al aprender SQL.
GROUP BY básico
-- Número de pedidos por estado
SELECT estado, COUNT(*) AS total
FROM pedidos
GROUP BY estado;
-- Cada fila del resultado representa un grupo (un valor distinto de estado)
-- Las columnas del SELECT deben ser: columnas del GROUP BY o funciones de agregado
-- Total facturado por cliente
SELECT
id_cliente,
COUNT(*) AS num_pedidos,
SUM(total) AS facturado,
MIN(fecha) AS primer_pedido,
MAX(fecha) AS ultimo_pedido
FROM pedidos
GROUP BY id_cliente;
GROUP BY con JOIN
-- Número de pedidos y facturación por cliente (con nombre)
SELECT
c.nombre,
c.ciudad,
COUNT(p.id) AS pedidos,
ROUND(SUM(p.total),2) AS facturado
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.id, c.nombre, c.ciudad
ORDER BY facturado DESC;
En el GROUP BY hay que incluir todas las columnas no-agregadas que aparecen en el SELECT. MySQL en modo estricto (ONLY_FULL_GROUP_BY) lo exige; si está desactivado, elige un valor arbitrario para las columnas no incluidas, lo que puede producir resultados incorrectos.
HAVING: filtrar grupos
-- Clientes con más de 2 pedidos
SELECT id_cliente, COUNT(*) AS pedidos
FROM pedidos
GROUP BY id_cliente
HAVING pedidos > 2;
-- Categorías con precio medio superior a 20€
SELECT
c.nombre AS categoria,
ROUND(AVG(p.precio), 2) AS precio_medio
FROM categorias c
JOIN productos p ON p.id_categoria = c.id
GROUP BY c.id, c.nombre
HAVING precio_medio > 20
ORDER BY precio_medio DESC;
WHERE vs HAVING: la diferencia clave
WHERE filtra filas antes de calcular los grupos. HAVING filtra grupos después de calcular los agregados. Nunca se pueden usar funciones de agregado en WHERE:
-- ERROR: no se puede usar COUNT() en WHERE
SELECT estado, COUNT(*) FROM pedidos
WHERE COUNT(*) > 1 -- ← ERROR
GROUP BY estado;
-- CORRECTO: usar HAVING para filtrar por agregado
SELECT estado, COUNT(*) AS total FROM pedidos
GROUP BY estado
HAVING total > 1;
-- Combinar WHERE y HAVING:
-- WHERE filtra pedidos antes de agrupar (solo pagados o enviados)
-- HAVING filtra grupos con más de 100€ de facturación
SELECT
id_cliente,
SUM(total) AS facturado
FROM pedidos
WHERE estado IN ('pagado', 'enviado')
GROUP BY id_cliente
HAVING facturado > 100
ORDER BY facturado DESC;
GROUP BY con múltiples columnas
-- Pedidos por año y mes
SELECT
YEAR(fecha) AS anyo,
MONTH(fecha) AS mes,
COUNT(*) AS pedidos,
SUM(total) AS facturado
FROM pedidos
GROUP BY YEAR(fecha), MONTH(fecha)
ORDER BY anyo, mes;
WITH ROLLUP: subtotales automáticos (MySQL)
-- Facturación por estado con total general al final
SELECT
COALESCE(estado, 'TOTAL') AS estado,
COUNT(*) AS pedidos,
SUM(total) AS facturado
FROM pedidos
GROUP BY estado WITH ROLLUP;
