lección 6
Agrupar datos: GROUP BY y HAVING
Agrupar filas por categoría y filtrar grupos — la herramienta que convierte datos en informes.
⏱ 55 min
Las funciones de agregación de la lección anterior son poderosas, pero aplicadas a toda la tabla dan un solo número global. "Las ventas totales son 3 millones" está bien, pero el director comercial quiere saber "¿cuánto vendemos en Madrid vs Barcelona?" y marketing pregunta "¿qué canal trae más clientes?". GROUP BY es la cláusula que convierte una tabla plana en un informe con categorías.
La analogía: imagina que tienes una caja gigante con todos los recibos del año. GROUP BY es como separar los recibos en montones — uno por ciudad, o uno por mes, o uno por vendedor. Luego cuentas/sumas cada montón por separado. El resultado es una fila por grupo con sus métricas.
### GROUP BY: dividir la tabla en grupos
Antes de agrupar, recuerda las tablas con las que trabajamos en esta lección. Son las mismas que creaste en la L02:
1-- Las dos tablas que usarás en los ejercicios de esta lección:23-- clientes (1000 filas)4-- id | nombre | email | ciudad | fecha_registro | canal_adquisicion | es_vip5-- ---|--------|-------|--------|----------------|-------------------|-------6-- 1 | Cliente_1 | cliente1@email.com | Barcelona | 2020-01-04 | google | false7-- 7 | Cliente_7 | cliente7@email.com | Valencia | 2020-01-22 | google | true ← cada 7 es VIP8-- Ciudades: Madrid, Barcelona, Valencia, Sevilla, Bilbao (cíclicas)9-- Canales: email, google, facebook, organico, referido (cíclicos)1011-- pedidos (5000 filas)12-- id | cliente_id | producto_id | cantidad | fecha | importe | estado13-- ---|-----------|-------------|----------|-------|---------|--------14-- 1 | 2 | 2 | 2 | 2023-01-02 | 47.13 | completado15-- Estados: 70% completado, 20% cancelado, 10% devuelto
Las columnas que necesitas para los ejercicios. Si no recuerdas el esquema, vuelve aquí.
GROUP BY toma una columna (o varias) y crea un "grupo" por cada valor único. Luego, las funciones de agregación se calculan DENTRO de cada grupo. Si agrupas por ciudad, obtienes una fila por ciudad con el COUNT, SUM, AVG de ESA ciudad:
1-- Número de clientes por ciudad2SELECT ciudad, COUNT(*) AS total_clientes3FROM clientes4GROUP BY ciudad5ORDER BY total_clientes DESC;67-- Ventas totales por estado del pedido8SELECT estado, COUNT(*) AS pedidos, SUM(importe)::INTEGER AS total9FROM pedidos10GROUP BY estado;1112-- Vendedores por equipo13SELECT equipo, COUNT(*) AS vendedores, SUM(ventas)::INTEGER AS total_ventas14FROM vendedores15GROUP BY equipo16ORDER BY total_ventas DESC;
GROUP BY: una fila por cada valor único de la columna agrupada
### La regla de oro de GROUP BY
Cuando usas GROUP BY, TODA columna en el SELECT debe ser: (a) parte del GROUP BY, o (b) una función de agregación. No puedes mezclar columnas individuales con agregados sin agrupar. Es un error que todo junior comete al menos una vez:
1-- ✅ CORRECTO: ciudad está en GROUP BY, COUNT es agregación2SELECT ciudad, COUNT(*) AS total3FROM clientes4GROUP BY ciudad;56-- ❌ ERROR: nombre NO está en GROUP BY ni es agregación7-- SELECT ciudad, nombre, COUNT(*) FROM clientes GROUP BY ciudad;8-- Error: column "nombre" must appear in GROUP BY clause910-- ✅ Si quieres nombre, agrúpalo también11SELECT ciudad, nombre, COUNT(*) AS total12FROM clientes13GROUP BY ciudad, nombre;
La regla: en SELECT solo columnas del GROUP BY o funciones de agregación
El error "column X must appear in the GROUP BY clause or be used in an aggregate function" es el error más frecuente al aprender GROUP BY. Léelo con calma: te dice exactamente qué columna olvidaste agrupar.
### Agrupar por múltiples columnas
Puedes agrupar por 2 o más columnas. Cada combinación única de valores genera un grupo. Es como hacer una tabla cruzada — ventas por ciudad Y por categoría, por ejemplo:
1-- Pedidos por estado y mes (2 columnas en GROUP BY)2SELECT3 estado,4 DATE_TRUNC('month', fecha) AS mes,5 COUNT(*) AS pedidos,6 SUM(importe)::INTEGER AS total7FROM pedidos8WHERE fecha >= '2024-01-01'9GROUP BY estado, DATE_TRUNC('month', fecha)10ORDER BY mes, estado;
GROUP BY con dos columnas: cada combinación única es un grupo
### HAVING: filtrar DESPUÉS de agrupar
WHERE filtra filas individuales ANTES de agrupar. Pero ¿qué haces cuando quieres filtrar los GRUPOS? "Ciudades con más de 1000 clientes", "categorías con ventas superiores a 100K"... No puedes usar WHERE porque la condición depende de la agregación. Para eso existe HAVING:
1-- Ciudades con más de 900 clientes2SELECT ciudad, COUNT(*) AS total_clientes3FROM clientes4GROUP BY ciudad5HAVING COUNT(*) > 9006ORDER BY total_clientes DESC;78-- Clientes que han gastado más de 2000€ en total9SELECT cliente_id, SUM(importe)::INTEGER AS gasto_total10FROM pedidos11WHERE estado = 'completado'12GROUP BY cliente_id13HAVING SUM(importe) > 200014ORDER BY gasto_total DESC15LIMIT 10;
HAVING filtra grupos, WHERE filtra filas. Orden: WHERE → GROUP → HAVING
### WHERE vs HAVING: la diferencia clave
- WHERE filtra FILAS INDIVIDUALES antes de agrupar. Ejemplo: "solo pedidos completados".
- HAVING filtra GRUPOS después de agrupar. Ejemplo: "solo ciudades con más de 100 pedidos".
- WHERE no puede usar funciones de agregación (COUNT, SUM, AVG...).
- HAVING SÍ puede usar funciones de agregación — para eso existe.
- Puedes usar AMBOS en la misma query: WHERE reduce filas, luego HAVING reduce grupos.
1-- WHERE + HAVING combinados:2-- "De los pedidos completados (WHERE), muestra solo3-- las ciudades con más de 500 pedidos (HAVING)"4SELECT c.ciudad, COUNT(*) AS pedidos, SUM(p.importe)::INTEGER AS total5FROM clientes c6JOIN pedidos p ON p.cliente_id = c.id7WHERE p.estado = 'completado' -- filtra filas ANTES de agrupar8GROUP BY c.ciudad9HAVING COUNT(*) > 500 -- filtra grupos DESPUÉS de agrupar10ORDER BY total DESC;
WHERE y HAVING trabajan juntos: primero filas, luego grupos
Truco mnemotécnico: WHERE trabaja con datos "crudos" (antes del resumen). HAVING trabaja con datos "cocidos" (después del resumen). Si tu condición usa COUNT, SUM, AVG → va en HAVING. Si no → va en WHERE.
### Orden de ejecución de SQL (modelo mental)
SQL NO se ejecuta en el orden en que lo escribes. El motor procesa las cláusulas en este orden interno, y entenderlo te ahorra muchos errores:
- 01.FROM — elige la tabla (o tablas con JOIN)
- 02.WHERE — filtra filas individuales
- 03.GROUP BY — agrupa las filas que pasaron el filtro
- 04.HAVING — filtra los grupos resultantes
- 05.SELECT — calcula las expresiones y columnas del resultado
- 06.ORDER BY — ordena el resultado final
- 07.LIMIT — recorta el número de filas devueltas
### FILTER: la alternativa elegante a COUNT(CASE WHEN...)
El patrón COUNT(CASE WHEN condicion THEN 1 END) funciona pero es verboso. En DuckDB y PostgreSQL (9.4+) existe una forma más limpia de contar subconjuntos: la cláusula FILTER. Hace exactamente lo mismo pero se lee mucho mejor:
1-- Forma clásica (verbosa pero universal)2SELECT3 ciudad,4 COUNT(*) AS total,5 COUNT(CASE WHEN es_vip THEN 1 END) AS vips6FROM clientes7GROUP BY ciudad;89-- Forma moderna con FILTER (DuckDB, PostgreSQL 9.4+)10SELECT11 ciudad,12 COUNT(*) AS total,13 COUNT(*) FILTER (WHERE es_vip) AS vips14FROM clientes15GROUP BY ciudad;
FILTER(WHERE...) es más legible: la condición va separada de la función de agregación
Consejo de senior: en mi equipo actual todos usamos FILTER porque el código se revisa más rápido — la condición está a la vista en vez de enterrada en un CASE WHEN. Pero si trabajas con MySQL, no lo tiene (solo DuckDB, PostgreSQL, SQLite 3.30+). Aprende ambas formas: COUNT(CASE WHEN...) es universal, FILTER es el futuro.
## ejercicios
Clientes por ciudad
El director comercial quiere saber cuántos clientes tenemos en cada ciudad y cuántos son VIP. Agrupa la tabla clientes por ciudad y muestra: ciudad, total de clientes, clientes VIP (COUNT con CASE) y porcentaje de VIPs. Ordena por total descendente.
Top 10 clientes por gasto
Finanzas quiere identificar los 10 clientes más valiosos. Agrupa pedidos completados por cliente_id, calcula el gasto total de cada uno y muestra solo los top 10.
Clientes valiosos con HAVING
El programa de fidelización necesita clientes que hayan gastado más de 500€ en total (solo completados). Muestra cliente_id, número de pedidos y gasto total. Usa HAVING para filtrar.
Evolución mensual de ventas
El CFO quiere ver la evolución mensual de 2024. Para cada mes, muestra: número de pedidos completados y total facturado. Usa DATE_TRUNC para agrupar por mes.
Rendimiento por canal de adquisición
Marketing quiere saber qué canal de adquisición trae más clientes. Agrupa por canal_adquisicion y muestra: canal, total de clientes, clientes VIP y la tasa de VIPs. Solo canales con más de 800 clientes (HAVING).
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...