Saltar al contenido

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:
2
3-- clientes (1000 filas)
4-- id | nombre | email | ciudad | fecha_registro | canal_adquisicion | es_vip
5-- ---|--------|-------|--------|----------------|-------------------|-------
6-- 1 | Cliente_1 | cliente1@email.com | Barcelona | 2020-01-04 | google | false
7-- 7 | Cliente_7 | cliente7@email.com | Valencia | 2020-01-22 | google | true ← cada 7 es VIP
8-- Ciudades: Madrid, Barcelona, Valencia, Sevilla, Bilbao (cíclicas)
9-- Canales: email, google, facebook, organico, referido (cíclicos)
10
11-- pedidos (5000 filas)
12-- id | cliente_id | producto_id | cantidad | fecha | importe | estado
13-- ---|-----------|-------------|----------|-------|---------|--------
14-- 1 | 2 | 2 | 2 | 2023-01-02 | 47.13 | completado
15-- 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 ciudad
2SELECT ciudad, COUNT(*) AS total_clientes
3FROM clientes
4GROUP BY ciudad
5ORDER BY total_clientes DESC;
6
7-- Ventas totales por estado del pedido
8SELECT estado, COUNT(*) AS pedidos, SUM(importe)::INTEGER AS total
9FROM pedidos
10GROUP BY estado;
11
12-- Vendedores por equipo
13SELECT equipo, COUNT(*) AS vendedores, SUM(ventas)::INTEGER AS total_ventas
14FROM vendedores
15GROUP BY equipo
16ORDER BY total_ventas DESC;

GROUP BY: una fila por cada valor único de la columna agrupada

De 20.000 filas individuales a 5 filas de resumen — eso es GROUP BY

### 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ón
2SELECT ciudad, COUNT(*) AS total
3FROM clientes
4GROUP BY ciudad;
5
6-- ❌ ERROR: nombre NO está en GROUP BY ni es agregación
7-- SELECT ciudad, nombre, COUNT(*) FROM clientes GROUP BY ciudad;
8-- Error: column "nombre" must appear in GROUP BY clause
9
10-- ✅ Si quieres nombre, agrúpalo también
11SELECT ciudad, nombre, COUNT(*) AS total
12FROM clientes
13GROUP 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)
2SELECT
3 estado,
4 DATE_TRUNC('month', fecha) AS mes,
5 COUNT(*) AS pedidos,
6 SUM(importe)::INTEGER AS total
7FROM pedidos
8WHERE 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 clientes
2SELECT ciudad, COUNT(*) AS total_clientes
3FROM clientes
4GROUP BY ciudad
5HAVING COUNT(*) > 900
6ORDER BY total_clientes DESC;
7
8-- Clientes que han gastado más de 2000€ en total
9SELECT cliente_id, SUM(importe)::INTEGER AS gasto_total
10FROM pedidos
11WHERE estado = 'completado'
12GROUP BY cliente_id
13HAVING SUM(importe) > 2000
14ORDER BY gasto_total DESC
15LIMIT 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 solo
3-- las ciudades con más de 500 pedidos (HAVING)"
4SELECT c.ciudad, COUNT(*) AS pedidos, SUM(p.importe)::INTEGER AS total
5FROM clientes c
6JOIN pedidos p ON p.cliente_id = c.id
7WHERE p.estado = 'completado' -- filtra filas ANTES de agrupar
8GROUP BY c.ciudad
9HAVING COUNT(*) > 500 -- filtra grupos DESPUÉS de agrupar
10ORDER 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:

  1. 01.FROM — elige la tabla (o tablas con JOIN)
  2. 02.WHERE — filtra filas individuales
  3. 03.GROUP BY — agrupa las filas que pasaron el filtro
  4. 04.HAVING — filtra los grupos resultantes
  5. 05.SELECT — calcula las expresiones y columnas del resultado
  6. 06.ORDER BY — ordena el resultado final
  7. 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)
2SELECT
3 ciudad,
4 COUNT(*) AS total,
5 COUNT(CASE WHEN es_vip THEN 1 END) AS vips
6FROM clientes
7GROUP BY ciudad;
8
9-- Forma moderna con FILTER (DuckDB, PostgreSQL 9.4+)
10SELECT
11 ciudad,
12 COUNT(*) AS total,
13 COUNT(*) FILTER (WHERE es_vip) AS vips
14FROM clientes
15GROUP 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

[01]

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.

Cargando editor...
[02]

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.

Cargando editor...
[03]

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.

Cargando editor...
[04]

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.

Cargando editor...
[05]

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).

Cargando editor...

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