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í
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.
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 escribe | Devuelve | Cuidado con |
|---|---|---|
| COUNT(*) | filas del grupo | un join que multiplica |
| COUNT(columna) | valores no nulos | confundirlo con el anterior |
| COUNT(DISTINCT columna) | valores distintos | es bastante más caro |
| AVG(columna) | promedio de los no nulos | si el nulo significaba cero, está mal |
| SUM(columna) | suma de los no nulos | sobre un grupo vacío devuelve NULL, no 0 |
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?
Práctica