Lección 9 de 16 · Nivel 3: Avanzado

SQL intermedio: GROUP BY, HAVING y JOIN

  • ⏱ 12 min de lectura
  • ❓ 4 preguntas de repaso
  • ✍️ Ejercicio con solución
  • 📥 Plantilla descargable
En esta lección
  1. Funciones de agregación
  2. GROUP BY: un resumen por grupo
  3. HAVING: filtra los grupos
  4. Agrupar por fechas y con CASE WHEN
  5. JOIN: combina tablas
  6. El orden en que se ejecuta una consulta
  7. Errores comunes (y qué hacer en su lugar)
  8. Resumen
Lo que lograrás en esta lección
  • Resumir datos con COUNT, SUM, AVG, MIN y MAX.
  • Agrupar resultados con GROUP BY y filtrar grupos con HAVING.
  • Distinguir cuándo usar WHERE y cuándo HAVING.
  • Unir tablas con INNER JOIN y LEFT JOIN, y usar LEFT JOIN para encontrar datos huérfanos.
  • Crear categorías con CASE WHEN y agrupar por mes o trimestre.
📥 Practica esta lección

Usa la base nexo.db que creaste en la lección anterior, con las tablas ventas, regiones y ventas_sin_limpiar.

En la lección anterior filtraste y ordenaste filas. Ahora darás el paso que convierte a SQL en una herramienta de análisis: resumir. Con GROUP BY obtienes en una consulta lo que en Excel hacías con una tabla dinámica (ventas por región, pedidos por canal), con HAVING filtras esos resúmenes y con JOIN combinas tablas, por ejemplo para analizar las ventas por país aunque el país esté en otra tabla. Estas tres herramientas aparecen en casi todas las pruebas técnicas de analista.

Funciones de agregación

Una función de agregación convierte muchas filas en un solo valor. Sin GROUP BY, resume la tabla entera:

SELECT COUNT(*)                  AS filas,
       COUNT(DISTINCT id_pedido) AS pedidos_unicos,
       ROUND(SUM(importe), 2)    AS ventas,
       ROUND(AVG(importe), 2)    AS ticket_promedio,
       MIN(importe)              AS minimo,
       MAX(importe)              AS maximo
FROM ventas;
filaspedidos_unicosventasticket_promediominimomaximo
19719738472.2195.2917.51298.0

Comparar COUNT(*) con COUNT(DISTINCT id_pedido) es la forma rápida de detectar duplicados en SQL: en la tabla ventas_sin_limpiar darían 200 y 197. Ten en cuenta que COUNT(columna) no cuenta los NULL: COUNT(importe) en la tabla sin limpiar devuelve 196, no 200.

GROUP BY: un resumen por grupo

GROUP BY parte la tabla en grupos (uno por cada valor distinto de la columna) y aplica la agregación a cada uno. Ventas y pedidos por región:

SELECT region,
       COUNT(*)               AS pedidos,
       ROUND(SUM(importe), 2) AS ventas
FROM ventas
GROUP BY region
ORDER BY ventas DESC;
regionpedidosventas
Ciudad de México418153.5
Lima337516.6
Nuevo León244211.0
Arequipa143803.6
La Libertad163415.5
Bogotá243317.8
Jalisco173210.2
Valle del Cauca142671.6
Antioquia142172.4
Ciudad de México21,2%
Lima19,5%
Nuevo León10,9%
Arequipa9,9%
La Libertad8,9%
Bogotá8,6%
Jalisco8,3%
Valle del Cauca6,9%
Antioquia5,6%

El resultado como % de las ventas totales. Colores por país: azul México, naranja Perú, verde Colombia. Las dos capitales suman el 40,7%.

La regla de oro de GROUP BY: cada columna del SELECT debe estar en el GROUP BY o dentro de una función de agregación. Si escribes SELECT region, producto, SUM(importe) ... GROUP BY region, la base de datos no sabe qué producto mostrar para cada región; muchos sistemas dan error y SQLite devuelve uno cualquiera, lo que es peor.

Puedes calcular varias métricas a la vez. Por canal:

SELECT canal,
       COUNT(*)               AS pedidos,
       ROUND(SUM(importe), 2) AS ventas,
       ROUND(AVG(importe), 2) AS ticket_promedio
FROM ventas
GROUP BY canal
ORDER BY ventas DESC;
canalpedidosventasticket_promedio
Web9417858.9189.99
Marketplace6011730.3195.5
Tienda física438883.0206.58

Detalle fino: el promedio exacto de Marketplace es 195,505. Excel lo muestra como 195,51, pero SQLite devuelve 195.5 porque guarda los decimales en binario (195,50499…). En sistemas financieros se usan columnas DECIMAL para evitar estas diferencias de centavos.

1. ¿Qué consulta da el número de pedidos y las unidades vendidas por categoría?

  • SELECT categoria, COUNT(*), SUM(unidades) FROM ventas;
  • SELECT categoria, COUNT(*), SUM(unidades) FROM ventas GROUP BY categoria;
  • SELECT categoria, unidades FROM ventas ORDER BY categoria;

Sin GROUP BY, la primera resume toda la tabla en una fila. La tercera lista las 197 filas sin resumir. La correcta devuelve 4 filas: Cómputo 44 pedidos y 71 unidades, Accesorios 79 y 241, Audio 50 y 133, Oficina 24 y 53.

HAVING: filtra los grupos

WHERE filtra filas antes de agrupar; HAVING filtra grupos después de agrupar, y por eso puede usar funciones de agregación. ¿Qué regiones vendieron más de 4.000 USD?

SELECT region, ROUND(SUM(importe), 2) AS ventas
FROM ventas
GROUP BY region
HAVING SUM(importe) > 4000
ORDER BY ventas DESC;
regionventas
Ciudad de México8153.5
Lima7516.6
Nuevo León4211.0

Y puedes combinar ambos. Esta consulta primero se queda con los pedidos web (WHERE), luego agrupa por región y, al final, conserva las regiones cuyas ventas web superan 2.000 USD (HAVING):

SELECT region, COUNT(*) AS pedidos, ROUND(SUM(importe), 2) AS ventas_web
FROM ventas
WHERE canal = 'Web'
GROUP BY region
HAVING SUM(importe) > 2000
ORDER BY ventas_web DESC;
regionpedidosventas_web
Lima133658.0
Ciudad de México212320.2
La Libertad82262.6
Arequipa82223.3

Tres de las cuatro regiones son peruanas: coincide con lo que viste en la tabla dinámica (en Perú, el 55,3% de las ventas es web).

2. Quieres las regiones con al menos 20 pedidos. ¿Dónde va la condición COUNT(*) >= 20?

  • En HAVING, porque filtra el resultado de una agregación.
  • En WHERE, porque es un filtro.
  • En ORDER BY, después de ordenar.

WHERE se aplica fila por fila, antes de agrupar, y no puede usar COUNT. HAVING se aplica a los grupos ya calculados. El resultado: Ciudad de México (41), Lima (33), Nuevo León (24) y Bogotá (24).

Agrupar por fechas y con CASE WHEN

CASE WHEN crea una columna nueva según condiciones, como un SI anidado de Excel. Es perfecto para crear trimestres, tramos de importe o segmentos:

SELECT CASE
         WHEN fecha < '2025-04-01' THEN 'T1'
         WHEN fecha < '2025-07-01' THEN 'T2'
         WHEN fecha < '2025-10-01' THEN 'T3'
         ELSE 'T4'
       END AS trimestre,
       COUNT(*) AS pedidos,
       ROUND(SUM(importe), 2) AS ventas
FROM ventas
GROUP BY trimestre
ORDER BY trimestre;
trimestrepedidosventas
T1376133.8
T2426985.8
T3489665.6
T47015687.0

Para agrupar por mes, cada sistema tiene su función: en SQLite strftime('%m', fecha), en PostgreSQL DATE_TRUNC('month', fecha), en MySQL MONTH(fecha). El cuarto trimestre, mes a mes, fue: octubre 4.798,8 (15 pedidos), noviembre 4.967,8 (23) y diciembre 5.920,4 (32).

JOIN: combina tablas

La tabla ventas tiene la región pero no el país; el país está en la tabla regiones. JOIN las une usando la columna que comparten, igual que BUSCARX en Excel, pero para todas las filas a la vez. Hay cuatro tipos principales:

INNER JOIN

Solo las filas que encuentran pareja en ambas tablas. Es el JOIN por defecto.

Riesgo: las filas sin pareja desaparecen sin aviso.

LEFT JOIN

Todas las filas de la tabla izquierda; si no hay pareja, las columnas de la derecha quedan en NULL.

Uso estrella: encontrar registros huérfanos o mal escritos.

RIGHT JOIN

Lo mismo, pero conservando todas las filas de la tabla derecha.

En la práctica: casi siempre se reescribe como LEFT JOIN cambiando el orden.

FULL OUTER JOIN

Todas las filas de ambas tablas, tengan o no pareja.

Útil para: conciliar dos listas, como inventario contra ventas.

Ventas por país con INNER JOIN. Las letras v y r son alias de tabla para escribir menos:

SELECT r.pais,
       COUNT(*)                 AS pedidos,
       ROUND(SUM(v.importe), 2) AS ventas,
       ROUND(AVG(v.importe), 2) AS ticket_promedio
FROM ventas AS v
JOIN regiones AS r ON v.region = r.region
GROUP BY r.pais
ORDER BY ventas DESC;
paispedidosventasticket_promedio
México8215574.7189.94
Perú6314735.7233.9
Colombia528161.8156.96

Los totales coinciden con los de Excel y la tabla dinámica: 197 pedidos y 38.472,20 USD. Cuando un JOIN cuadra con una cifra conocida, puedes confiar en él.

LEFT JOIN para encontrar datos huérfanos

Ahora haz el JOIN con la tabla sin limpiar. Con INNER JOIN obtienes 192 filas en lugar de 200: ocho pedidos desaparecen sin ningún mensaje. Para ver cuáles, usa LEFT JOIN y quédate con los que no encontraron país:

SELECT '[' || s.region || ']' AS region, COUNT(*) AS filas
FROM ventas_sin_limpiar AS s
LEFT JOIN regiones AS r ON s.region = r.region
WHERE r.pais IS NULL
GROUP BY s.region
ORDER BY filas DESC, s.region;
regionfilas
[CDMX]2
[lima]2
[Bogota]1
[Lima ]1
[Nuevo Leon]1
[antioquia]1

Los corchetes (concatenados con ||) hacen visible el espacio de «Lima ». Este patrón, LEFT JOIN + IS NULL, es una de las comprobaciones de calidad más útiles que existen.

Cuidado: un JOIN también puede multiplicar filas

Si la tabla regiones tuviera «Lima» repetida dos veces, cada pedido de Lima se uniría con ambas filas y sus ventas aparecerían duplicadas. Antes de unir, comprueba que la columna de unión sea única en la tabla de búsqueda: SELECT region, COUNT(*) FROM regiones GROUP BY region HAVING COUNT(*) > 1; debe devolver cero filas.

3. Unes ventas_sin_limpiar (200 filas) con regiones usando INNER JOIN y obtienes 192 filas. ¿Qué pasó?

  • Se eliminaron automáticamente los 8 duplicados.
  • 8 filas tienen regiones que no coinciden exactamente con la tabla regiones y quedaron fuera.
  • El JOIN falló y hay que repetirlo.

INNER JOIN solo conserva las filas con pareja. «CDMX», «lima», «Lima », «Bogota», «Nuevo Leon» y «antioquia» no tienen pareja exacta. Los duplicados son otro problema (son 3, y el JOIN los conserva).

El orden en que se ejecuta una consulta

  1. FROM y JOINArma la tabla de trabajoToma las tablas y las une.
  2. WHEREFiltra filasDescarta las filas que no cumplen la condición.
  3. GROUP BYForma gruposUna «caja» por cada valor distinto.
  4. HAVINGFiltra gruposDescarta grupos según sus agregados.
  5. SELECTCalcula columnasAgregaciones, alias y CASE.
  6. ORDER BY y LIMITOrdena y cortaDevuelve el resultado final.

Errores comunes (y qué hacer en su lugar)

Poner en SELECT columnas que no están en GROUP BY ni agregadas.

Agrega la columna al GROUP BY o envuélvela en una agregación.

Filtrar una agregación con WHERE (WHERE SUM(importe) > 4000).

Usa HAVING para condiciones sobre grupos.

Usar INNER JOIN sin comprobar cuántas filas se pierden.

Compara el conteo antes y después, o usa LEFT JOIN con IS NULL.

Unir por una columna que se repite en la tabla de búsqueda.

Verifica que la clave sea única antes del JOIN.

4. ¿Qué devuelve SELECT producto, COUNT(*) AS pedidos FROM ventas GROUP BY producto HAVING COUNT(*) > 20 ORDER BY pedidos DESC;?

  • Los 10 productos con su número de pedidos.
  • Los pedidos con más de 20 unidades.
  • Los 5 productos con más de 20 pedidos, del que más tiene al que menos.

HAVING deja solo los productos con más de 20 pedidos: Mouse inalámbrico (32), Audífonos Bluetooth (29), Mochila para laptop (24), Teclado mecánico (23) y Parlante portátil (21). Ninguno es de Cómputo: los productos caros se piden menos veces.

Ejercicio práctico

Escribe una consulta que muestre, para cada combinación de país y canal, el número de pedidos y las ventas, solo para las combinaciones con 20 pedidos o más, ordenadas de mayor a menor venta. ¿Cuál es la combinación más importante y qué país no aparece con su tienda física?

Ver solución
SELECT r.pais, v.canal,
       COUNT(*)                 AS pedidos,
       ROUND(SUM(v.importe), 2) AS ventas
FROM ventas AS v
JOIN regiones AS r ON v.region = r.region
GROUP BY r.pais, v.canal
HAVING COUNT(*) >= 20
ORDER BY ventas DESC;
paiscanalpedidosventas
PerúWeb298143.9
MéxicoWeb385878.6
MéxicoMarketplace245566.1
PerúMarketplace224896.3
MéxicoTienda física204130.0
ColombiaWeb273836.4

Perú-Web es la combinación que más vende (8.143,90 USD) con menos pedidos que México-Web: su ticket es mucho mayor. Perú y Colombia no aparecen con Tienda física porque tienen menos de 20 pedidos en ese canal. Fíjate en que el GROUP BY lleva las dos columnas no agregadas del SELECT.

Resumen

  • COUNT, SUM, AVG, MIN y MAX resumen filas; COUNT(DISTINCT) ayuda a detectar duplicados.
  • GROUP BY crea un resumen por grupo; toda columna no agregada del SELECT debe estar en él.
  • WHERE filtra filas antes de agrupar; HAVING filtra grupos después.
  • JOIN combina tablas: INNER conserva solo coincidencias y LEFT conserva toda la tabla izquierda.
  • LEFT JOIN + IS NULL encuentra datos huérfanos: en la tabla sin limpiar, 8 filas sin país.

En la próxima lección: Visualización de datos: cómo elegir el gráfico correcto. Convertirás estos resultados en gráficos que se entienden de un vistazo.

🎓 Guarda tu avance y obtén tu certificado gratis

Crea tu cuenta en 30 segundos, marca tus lecciones y descarga el certificado al terminar.

Inscribirme gratis