Atlasingeniería

Bases de datosSQLTema 3

Agregaciones, GROUP BY y HAVING

Agrupar colapsa muchas filas en una, y de ahí salen las tres preguntas que siempre aparecen: qué se puede poner en el SELECT, dónde va cada filtro y por qué faltan los grupos vacíos.

Para este tema conviene tener claro:Joins: cómo pensarlos

Casi todo reporte es la misma operación: juntar filas por algún criterio y calcular un número por grupo. GROUP BY hace exactamente eso, y entenderlo como “colapsar filas en una” resuelve de una vez las confusiones que arrastra.

Las funciones que resumen muchas filas en una

Las funciones de agregación toman muchos valores y devuelven uno: COUNT, SUM, AVG, MIN, MAX. Todas ignoran los nulos, con una excepción que confunde: COUNT(*) cuenta filas, mientras que COUNT(columna) cuenta valores no nulos de esa columna.

Esa diferencia es un clásico: un promedio calculado con AVG sobre una columna con nulos divide por la cantidad de valores presentes, no por la cantidad de filas. Si los nulos tenían que contar como cero, hay que decirlo.

Antes de seguir, predecí

Un promedio de montos sobre una columna donde algunas filas tienen nulo. ¿Sobre cuántas filas promedia?

Qué hace GROUP BY con las filas

GROUP BY parte las filas en grupos según los valores de las columnas indicadas, y devuelve una fila por grupo. De ahí sale la regla que todos chocan al principio: en el SELECT sólo pueden aparecer las columnas por las que se agrupó, o funciones de agregación.

El motivo es simple: si un grupo tiene cien filas con cien valores distintos en una columna, ¿cuál de los cien mostraría? La pregunta no tiene respuesta, y por eso el motor la rechaza. Algunos dialectos lo permiten y devuelven un valor arbitrario, que es peor que un error.

La tabla, con un sueldo sin cargar. Ese nulo es el que va a hacer toda la diferencia.

1 / 5
Fijate el grupo de Córdoba: tres filas, dos sueldos. COUNT(*) dice 3, COUNT(sueldo) dice 2, y AVG divide por 2. Si el nulo significaba «cobra cero», el promedio que sale de acá está mal, y la base no tiene forma de saberlo.

Filtrar antes o después de agrupar

Hay dos lugares para filtrar y hacen cosas distintas. WHERE filtra filas antes de agrupar; HAVING filtra grupos después, y es el único que puede usar agregaciones.

“Ventas de 2026 de los clientes que compraron más de diez veces” tiene los dos: el año va en el WHERE, el conteo en el HAVING. Y hay un motivo de rendimiento para no confundirlos: lo que se filtra en el WHERE nunca llega a agruparse, así que el trabajo es menor.

Los meses que faltan en el reporte

El problema más reportado de los reportes: faltan meses. GROUP BY sólo puede agrupar filas que existen, así que un mes sin ventas no aparece como cero, simplemente no aparece.

La solución es generar la lista completa de períodos —con una tabla de calendario o una función generadora— y hacer LEFT JOIN contra los datos, completando con COALESCE los nulos como cero. Es el caso donde el outer join y las agregaciones se combinan, y conviene tenerlo escrito de antemano.

Cuándo COUNT(DISTINCT) y cuándo no

COUNT(DISTINCT columna) cuenta valores distintos, y es más caro que COUNT porque exige ordenar o hashear. Conviene usarlo a conciencia y no por costumbre.

Un DISTINCT que aparece “para sacar duplicados” suele ser un síntoma, no una solución: casi siempre los duplicados los generó un join uno a muchos, y taparlos con DISTINCT puede además borrar duplicados legítimos. Conviene arreglar el join.

Subtotales sin repetir la consulta

SQL tiene extensiones útiles para reportes. ROLLUP agrega subtotales y total general en la misma consulta; GROUPING SETS permite varias agrupaciones distintas de una sola pasada, en vez de unir varias consultas.

Y la agregación condicional resuelve las tablas cruzadas sin dialectos raros: SUM(CASE WHEN estado = 'pagada' THEN importe ELSE 0 END) produce una columna por categoría dentro del mismo grupo. Es el patrón más portable para pivotar.

WHERE, GROUP BY y HAVING en una consulta

SELECT c.city,
       COUNT(*)              AS pedidos,
       COUNT(o.discount)     AS con_descuento,   -- ignora los nulos
       SUM(o.total)          AS facturado,
       AVG(o.total)          AS ticket_promedio
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-01-01'   -- filtra filas, antes de agrupar
GROUP BY c.city
HAVING SUM(o.total) > 100000        -- filtra grupos, después de agregar
ORDER BY facturado DESC;

Las dos líneas de filtro hacen cosas distintas y el orden lo explica. El WHERE corre antes de agrupar, así que decide qué filas entran a cada grupo; el HAVING corre después, así que decide qué grupos sobreviven y puede usar funciones de agregación. Poner una condición sobre la fecha en el HAVING funciona en algunos motores y es mucho más lento: agrupa todo para después descartar.

Las dos formas de COUNT en el mismo SELECT muestran la diferencia que más confunde: una cuenta filas y la otra cuenta valores presentes, y la resta entre las dos es exactamente la cantidad de nulos.

Los promedios que mienten

Se escribeDevuelveCuidado con
COUNT(*)filas del grupoun join que multiplica
COUNT(columna)valores no nulosconfundirlo con el anterior
COUNT(DISTINCT columna)valores distintoses bastante más caro
AVG(columna)promedio de los no nulossi el nulo significaba cero, está mal
SUM(columna)suma de los no nulossobre un grupo vacío devuelve NULL, no 0
Las últimas dos filas son las que producen reportes equivocados: un promedio que divide por menos filas de las que uno cree, y una suma que devuelve nulo donde el informe esperaba un cero.

Cierre

Agrupar colapsa filas: en el SELECT sólo entra lo que se agrupó o se agregó. WHERE filtra antes y HAVING después. Los grupos sin filas no aparecen y hay que generarlos aparte, y un DISTINCT inesperado suele ser un join mal hecho.

Autoevaluación

¿Lo entendiste?

¿Qué diferencia hay entre COUNT(*) y COUNT(columna)?
¿Por qué en el SELECT sólo pueden ir las columnas agrupadas o agregaciones?
WHERE y HAVING, ¿en qué se diferencian?
¿Cuál es la forma más útil de pensar GROUP BY?