Cuando agrupas datos con GROUP BY y utilizas funciones de agregación (COUNT, SUM, AVG, etc.), a menudo necesitas filtrar esos grupos (por ejemplo, «mostrar sólo departamentos con más de 5 empleados» o «mostrar productos con ventas totales superiores a 100k»). Para eso existe la cláusula HAVING: que permite aplicar condiciones sobre resultados agregados, es decir, filtrar grupos en lugar de filas.
En este artículo verás:
- la diferencia práctica entre
WHEREyHAVING, - la sintaxis y varios ejemplos,
- combinaciones
WHERE + GROUP BY + HAVING, - buenas prácticas y recomendaciones de rendimiento,
- notas de compatibilidad entre SQL Server, PostgreSQL y MySQL.
Todos los ejemplos son ejecutables en db-fiddle.com.
¿Qué hace HAVING y cómo se diferencia de WHERE?
WHEREfiltra filas individuales antes de que se agrupe la información.HAVINGfiltra los grupos resultantes deGROUP BY, después de calcular agregados.
Regla simple: usa WHERE para condiciones sobre columnas no agregadas; usa HAVING para condiciones que involucran funciones agregadas.
Sintaxis básica
SELECT columna_agrupada, AGG(columna) AS alias
FROM tabla
GROUP BY columna_agrupada
HAVING condición_agregada; Ejemplo mínimo:
SELECT departamento, COUNT(*) AS total_empleados
FROM empleados
GROUP BY departamento
HAVING COUNT(*) > 5; Estructura y datos para practicar
CREATE TABLE departamentos (
id_departamento INTEGER PRIMARY KEY,
nombre_departamento TEXT NOT NULL
);
CREATE TABLE empleados (
id_empleado INTEGER PRIMARY KEY,
nombre TEXT NOT NULL,
salario DECIMAL(10,2) NOT NULL,
id_departamento INTEGER
);
INSERT INTO departamentos (id_departamento, nombre_departamento) VALUES
(1,'Ventas'),(2,'Marketing'),(3,'Finanzas'),(4,'Desarrollo');
INSERT INTO empleados (id_empleado, nombre, salario, id_departamento) VALUES
(1,'Ana',70000,1),
(2,'Luis',90000,1),
(3,'Marta',60000,2),
(4,'Carlos',50000,3),
(5,'Sofía',80000,1),
(6,'Diego',45000,NULL),
(7,'Laura',72000,4),
(8,'Pedro',48000,2); Ejemplo A — Contar empleados por departamento y filtrar grupos
SELECT d.nombre_departamento, COUNT(e.id_empleado) AS total_empleados
FROM empleados e
JOIN departamentos d ON e.id_departamento = d.id_departamento
GROUP BY d.nombre_departamento
HAVING COUNT(e.id_empleado) > 2; Explicación: primero se agrupa por departamento, luego HAVING selecciona sólo los departamentos con más de 2 empleados.
Ejemplo B — SUM con HAVING (total salarial por departamento)
SELECT d.nombre_departamento, SUM(e.salario) AS total_salarios
FROM empleados e
JOIN departamentos d ON e.id_departamento = d.id_departamento
GROUP BY d.nombre_departamento
HAVING SUM(e.salario) >= 200000; Explicación: útil para reportes financieros por unidad.
Ejemplo C — WHERE + GROUP BY + HAVING (filtrado previo y por agregados)
SELECT d.nombre_departamento, AVG(e.salario) AS salario_promedio
FROM empleados e
JOIN departamentos d ON e.id_departamento = d.id_departamento
WHERE e.salario > 50000 -- filtra filas antes de agrupar
GROUP BY d.nombre_departamento
HAVING AVG(e.salario) > 70000; -- filtra grupos según el agregado Ejemplo D — Nota sobre alias en HAVING (portabilidad)
Algunos motores permiten usar un alias en HAVING, p. ej.:
SELECT d.nombre_departamento, COUNT(*) AS total
FROM empleados e
JOIN departamentos d ON e.id_departamento = d.id_departamento
GROUP BY d.nombre_departamento
HAVING total > 2; -- MySQL suele permitirlo Recomendación: por portabilidad usa la función agregada directa en HAVING (HAVING COUNT(*) > 2) en lugar del alias.
Casos especiales
- HAVING sin GROUP BY: MySQL permite
HAVINGsinGROUP BY(comportamiento no estándar: trata el conjunto como un único grupo), pero esto no es portable ni recomendable. - Usar HAVING para filtrar filas individuales (por ejemplo,
HAVING salario > 50000sin agregación) es conceptualmente incorrecto: en esos casos usaWHERE.
Rendimiento y buenas prácticas
- Filtra antes con WHERE: reduce el conjunto de filas antes de agrupar para ahorrar trabajo en el agregador.
- Usa HAVING sólo para condiciones agregadas (COUNT, SUM, AVG…).
- Indexes: garantizar índices en columnas usadas en
JOINo enWHEREmejora la velocidad previa a la agregación; las agregaciones en sí mismas seguirán necesitando procesamiento. - Evita funciones en la columna de agrupamiento (p. ej.
GROUP BY YEAR(fecha)) si puedes reescribir la condición con rangos (BETWEEN) — las funciones impiden usar índices. - Comprueba el plan de ejecución en consultas pesadas (EXPLAIN / EXPLAIN ANALYZE) y evalúa el uso de CTEs o agregaciones parciales si corresponde.
- Para grandes volúmenes, evaluar pre-aggregaciones o tablas resumen puede ser más eficiente que hacer agregación on-the-fly.
Diferencias entre motores (SQL Server, PostgreSQL, MySQL)
- SQL Server
HAVINGes estándar y funciona como se espera.- Para optimización puedes usar índices, vistas indexadas (indexed views) y revisar el plan de ejecución.
- PostgreSQL
- Soporta
HAVINGy, además, la cláusulaFILTER (WHERE ...)para agregados es muy útil:SELECT COUNT(*) FILTER (WHERE estado = 'activo') AS activos, COUNT(*) FILTER (WHERE estado = 'inactivo') AS inactivos FROM usuarios; - PostgreSQL suele ser muy eficiente en agregaciones con configuración adecuada y estadísticas.
- Soporta
- MySQL
- Soporta
HAVING. - MySQL históricamente permite
HAVINGsinGROUP BY(comportamiento no estándar). - Para portabilidad y claridad, prefiera usar la expresión agregada en
HAVINGen vez de alias.
- Soporta
Conclusión
HAVING es la herramienta correcta cuando necesitas filtrar resultados agregados después de aplicar GROUP BY. Para consultas eficientes: filtra primero con WHERE, agrupa con GROUP BY y luego aplica HAVING sólo a condiciones agregadas. Practica los ejemplos en db-fiddle.com para interiorizar la lógica y medir rendimiento real.