SQL y datos

GROUP BY y funciones de agregación en SQL

Aprende GROUP BY en SQL con SUM, COUNT, AVG, MIN y MAX: cómo afectan los NULL, WHERE vs HAVING, agrupar por varias columnas y totales tipo pivot.

Por Equipo editorial de DashCore HubActualizado 8 min de lectura

GROUP BY agrupa muchas filas en una sola fila por grupo (por región, por producto, por mes) para que puedas calcular totales, conteos y promedios de cada grupo con funciones de agregación como SUM, COUNT, AVG, MIN y MAX. Agrega HAVING cuando necesites filtrar los grupos mismos, por ejemplo "solo los vendedores con al menos $300 en ventas". Es el equivalente en SQL de una tabla dinámica y está detrás de casi todos los números de un dashboard de negocio.

La tabla de ventas de ejemplo

Todos los ejemplos usan esta pequeña tabla sales (ventas). Fíjate que la venta 6 tiene un monto vacío (NULL), algo común en datos reales que afecta algunos resultados:

sale_idregionrepproductsale_dateunitsamount
1EastAnaCoffee2026-09-0310250
2EastAnaTea2026-09-10460
3EastLuisCoffee2026-09-126150
4WestMiaCoffee2026-09-1512300
5WestMiaTea2026-09-228120
6WestOmarSnacks2026-09-2520NULL
7SouthAnaTea2026-09-28230

Las cinco funciones de agregación que más usarás

Las funciones de agregación toman muchos valores y devuelven uno. Sin GROUP BY, resumen toda la tabla:

SELECT
    COUNT(*)      AS total_rows,
    COUNT(amount) AS rows_with_amount,
    SUM(amount)   AS total_sales,
    AVG(amount)   AS avg_sale,
    MIN(amount)   AS smallest_sale,
    MAX(amount)   AS largest_sale
FROM sales;

Resultado: 7 filas, 6 filas con monto, total 910, promedio de aproximadamente 151.67, mínimo 30 y máximo 300.

FunciónQué devuelveCómo trata los NULL
COUNT(*)Número de filasCuenta todas las filas
COUNT(columna)Número de valores no nulosIgnora los NULL
COUNT(DISTINCT columna)Número de valores distintosIgnora los NULL
SUM(columna)TotalIgnora los NULL
AVG(columna)Promedio de los valores no nulosIgnora los NULL (no los cuenta como cero)
MIN / MAX(columna)Valor más pequeño / más grandeIgnora los NULL

Esa última columna es importante: aquí el promedio es 910 ÷ 6, no 910 ÷ 7, porque la fila con NULL se ignora. Si un monto vacío realmente debe contar como cero, escribe AVG(COALESCE(amount, 0)).

GROUP BY: una fila por grupo

Agrega GROUP BY y las agregaciones se calculan por separado para cada grupo:

SELECT
    region,
    COUNT(*)    AS sales_count,
    SUM(amount) AS total_sales,
    AVG(amount) AS avg_sale
FROM sales
GROUP BY region
ORDER BY total_sales DESC;
regionsales_counttotal_salesavg_sale
East3460153.33
West3420210.00
South13030.00

El promedio de West es 210 (420 ÷ 2) porque la venta de Omar no tiene monto. Este es justo el tipo de detalle que hace que dos dashboards muestren un "ticket promedio" distinto.

La regla de oro

Cada columna del SELECT debe estar en la lista del GROUP BY o dentro de una función de agregación. SELECT region, rep, SUM(amount) FROM sales GROUP BY region da error en PostgreSQL y SQL Server, porque la base de datos no sabe qué vendedor mostrar en una región que tiene dos. (Algunos motores, como SQLite y MySQL con ciertas configuraciones, la aceptan y eligen un valor cualquiera, lo cual es todavía más peligroso.)

Agrupar por varias columnas

SELECT region, product, SUM(units) AS units
FROM sales
GROUP BY region, product
ORDER BY region, product;

Esto devuelve una fila por cada combinación de región y producto que existe en los datos: East-Coffee 16, East-Tea 4, South-Tea 2, West-Coffee 12, West-Snacks 20 y West-Tea 8.

WHERE vs HAVING

Ambos filtran, pero en momentos distintos. WHERE filtra filas antes de agrupar; HAVING filtra grupos después de agregar, así que puede usar funciones de agregación.

SELECT rep, SUM(amount) AS total_sales
FROM sales
GROUP BY rep
HAVING SUM(amount) >= 300
ORDER BY total_sales DESC;

Resultado: Mia 420 y Ana 340. Luis (150) y Omar (sin monto) quedan fuera.

Puedes usar ambos en la misma consulta. Esta excluye primero los snacks y luego conserva solo las regiones que todavía tienen al menos dos ventas:

SELECT region, SUM(amount) AS total_sales
FROM sales
WHERE product <> 'Snacks'
GROUP BY region
HAVING COUNT(*) >= 2;

Resultado: East 460 y West 420. El orden lógico completo es FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.

Consejo: Filtra con WHERE siempre que la condición no necesite una agregación. Quitar filas antes de agrupar suele ser más rápido y deja más clara tu intención que poner todo en HAVING.

Patrones prácticos para reportes de negocio

Contar clientes o vendedores únicos

SELECT COUNT(DISTINCT rep) FROM sales; devuelve 4. Los conteos distintos son la forma de medir clientes activos, compradores únicos o SKUs activos.

Totales condicionales (un pivot en SQL)

Combina SUM con CASE para convertir filas en columnas, igual que una tabla dinámica:

SELECT
    region,
    SUM(CASE WHEN product = 'Coffee' THEN amount ELSE 0 END) AS coffee_sales,
    SUM(CASE WHEN product = 'Tea'    THEN amount ELSE 0 END) AS tea_sales
FROM sales
GROUP BY region
ORDER BY region;

Resultado: East 400 / 60, South 0 / 30, West 300 / 120. Si ya usas tablas dinámicas, nuestra guía de tablas dinámicas en Excel muestra la misma idea desde la hoja de cálculo.

Totales por mes

Las funciones de fecha cambian según la base de datos. Para agrupar por mes, usa DATE_TRUNC('month', sale_date) en PostgreSQL, DATETRUNC(month, sale_date) en SQL Server 2022 o posterior (o YEAR() y MONTH() en versiones anteriores) y strftime('%Y-%m', sale_date) en SQLite. Agrupa por la misma expresión que pones en el SELECT.

Subtotales y total general con ROLLUP

Los reportes suelen necesitar subtotales, como las filas de "Total" de una tabla dinámica. ROLLUP los agrega por ti:

-- PostgreSQL y SQL Server
SELECT region, product, SUM(amount) AS total_sales
FROM sales
GROUP BY ROLLUP (region, product)
ORDER BY region, product;

-- MySQL
SELECT region, product, SUM(amount) AS total_sales
FROM sales
GROUP BY region, product WITH ROLLUP;

Además de las filas normales de región y producto, el resultado incluye una fila de subtotal por región (con product en NULL) y una fila de total general (con ambas columnas en NULL) igual a 910. Envuelve las columnas en COALESCE(region, 'Todas las regiones') si quieres etiquetas legibles, pero ten en cuenta que un NULL real en tus datos recibiría la misma etiqueta.

Agrega antes de llegar al dashboard

Si tu tabla de origen tiene millones de transacciones pero tu dashboard solo muestra totales diarios por tienda, una consulta con GROUP BY (o una vista en la base de datos) que preagregue los datos puede hacer tu reporte mucho más liviano y rápido. Nuestra guía para conectar Power BI a bases de datos SQL explica cuándo conviene este enfoque y cuándo es mejor conservar el detalle.

Atajo con IA: Describe el reporte que quieres y las columnas de la tabla, y pide el SQL junto con una verificación: "Tabla sales(region, rep, product, sale_date, units, amount) en PostgreSQL. Escribe una consulta con las ventas totales y el número de vendedores distintos por región y mes en 2026, solo para los grupos con más de 1,000 en ventas. Luego dime qué filas elimina el WHERE y qué grupos elimina el HAVING." Pedir la explicación te ayuda a detectar errores de lógica.

Errores comunes

  • Usar una agregación en WHERE. WHERE SUM(amount) > 300 da error; usa HAVING.
  • Olvidar cómo se comportan los NULL. AVG y COUNT(columna) ignoran los NULL, así que los resultados pueden ser distintos a lo que esperas.
  • Agrupar al nivel equivocado. Si agregas una columna de más al GROUP BY (como sale_id), obtienes una fila por venta y en realidad no resumes nada.
  • Contar doble después de un JOIN. Si un JOIN duplica filas, SUM duplica los totales. Revisa JOINs en SQL explicados para saber cómo detectarlo.

Próximos pasos

Recrea la tabla de ejemplo y ejecuta cada consulta, comparando con los resultados de arriba. Luego arma un reporte semanal con tus propios datos: totales por región, clientes distintos y un filtro con HAVING. Cuando necesites rankings o acumulados sin colapsar las filas, continúa con las funciones de ventana en SQL.

Mira: lecciones en video

Cómo Agrupar Datos con GROUP BY en SQLpor DataBoosters · Ver en YouTube ↗
Advanced Aggregate Functions in SQL (GROUP BY, HAVING vs. WHERE)por Becoming a Data Scientist · Ver en YouTube ↗Video en inglés — activa los subtítulos traducidos en YouTube si los necesitas.

Preguntas frecuentes

¿Cuál es la diferencia entre WHERE y HAVING?

WHERE filtra filas individuales antes de agruparlas, mientras que HAVING filtra grupos después de calcular las agregaciones. Solo HAVING puede usar funciones como SUM o COUNT en su condición.

¿COUNT(*) cuenta los valores NULL?

COUNT(*) cuenta todas las filas, incluso las que tienen NULL. COUNT(columna) cuenta solo las filas donde esa columna no es NULL.

¿Por qué mi consulta dice que una columna debe estar en GROUP BY?

Cada columna del SELECT debe estar agrupada o agregada. Agrégala al GROUP BY o envuélvela en una función de agregación como MAX o SUM.

¿AVG trata los NULL como cero?

No. AVG ignora por completo los valores NULL. Usa AVG(COALESCE(columna, 0)) si los valores faltantes deben contar como cero.

¿Aprendes con la Academia?
Marca esta lección como completada para seguir tu progreso.
Hacer el quiz