HAVING COUNT
La combinación de HAVING con COUNT es uno de los patrones más útiles en SQL. Permite responder preguntas como "¿qué valores aparecen más de una vez?", "¿qué clientes han hecho exactamente un pedido?" o "¿qué productos no se han vendido nunca?". En este artículo exploramos los patrones más habituales.
Encontrar duplicados
Uno de los usos clásicos de HAVING COUNT es detectar valores duplicados. Si quisieras comprobar si hay ciudades con más de un cliente:
SELECT ciudad, COUNT(*) AS total_clientes
FROM clientes
GROUP BY ciudad
HAVING total_clientes > 1
ORDER BY total_clientes DESC;| ciudad | total_clientes |
|---|---|
| Madrid | 2 |
| Barcelona | 2 |
| Valencia | 2 |
| Zaragoza | 2 |
Cuatro ciudades tienen más de un cliente. Este patrón es especialmente útil cuando revisas datos antes de añadir una restricción UNIQUE, o para encontrar registros que podrían estar duplicados por error.
Encontrar valores que aparecen exactamente una vez
El caso opuesto: encontrar etiquetas que solo se han asignado a un producto:
SELECT etiqueta, COUNT(*) AS veces_usada
FROM etiquetas_producto
GROUP BY etiqueta
HAVING veces_usada = 1
ORDER BY etiqueta;| etiqueta | veces_usada |
|---|---|
| básico | 1 |
| deporte | 1 |
| ergonómico | 1 |
| fitness | 1 |
| gaming | 1 |
Estas etiquetas solo se usan en un producto. Podrían ser candidatas a eliminarse o fusionarse con etiquetas más generales.
Clientes por número de pedidos
¿Cuántos clientes han hecho 1 pedido, cuántos han hecho 2, cuántos 3?
SELECT
pedidos_por_cliente AS num_pedidos,
COUNT(*) AS num_clientes
FROM (
SELECT cliente_id, COUNT(*) AS pedidos_por_cliente
FROM pedidos
GROUP BY cliente_id
) AS subquery
GROUP BY pedidos_por_cliente
ORDER BY pedidos_por_cliente;| num_pedidos | num_clientes |
|---|---|
| 1 | 13 |
| 2 | 5 |
| 3 | 1 |
13 clientes han hecho un solo pedido, 5 han hecho 2 pedidos, y 1 cliente (María) ha hecho 3. Usamos una subconsulta porque necesitamos agrupar dos veces: primero por cliente y después por número de pedidos.
Productos con múltiples reseñas
Productos que tienen más de una reseña, mostrando el promedio de puntuación:
SELECT
p.nombre,
COUNT(*) AS num_resenas,
ROUND(AVG(r.puntuacion), 1) AS media
FROM resenas r
JOIN productos p ON r.producto_id = p.id
GROUP BY p.id, p.nombre
HAVING num_resenas > 1
ORDER BY num_resenas DESC, media DESC;| nombre | num_resenas | media |
|---|---|---|
| iPhone 15 Pro | 2 | 4.5 |
| Camiseta algodón básica | 2 | 3.5 |
| Zapatillas running pro | 2 | 4.5 |
| Clean Code | 2 | 4.5 |
Cuatro productos tienen 2 reseñas cada uno. El iPhone 15 Pro, las zapatillas running pro y Clean Code comparten la mejor media (4.5).
Empleados con pocos pedidos gestionados
Encontrar empleados que han gestionado menos de 5 pedidos en un periodo:
SELECT
e.nombre,
e.puesto,
COUNT(p.id) AS pedidos_gestionados
FROM empleados e
LEFT JOIN pedidos p ON e.id = p.empleado_id
AND p.fecha_pedido >= '2025-12-01'
GROUP BY e.id, e.nombre, e.puesto
HAVING pedidos_gestionados < 5
ORDER BY pedidos_gestionados;| nombre | puesto | pedidos_gestionados |
|---|---|---|
| Ricardo | Director General | 0 |
| Alberto | Director de Logística | 0 |
| Raúl | Vendedor Junior | 0 |
| Cristina | Responsable de Almacén | 0 |
| Marcos | Operario de Almacén | 0 |
| Inés | Operaria de Almacén | 0 |
| Sofía | Directora de Ventas | 0 |
| Patricia | Vendedora | 4 |
| Daniel | Vendedor | 4 |
| Natalia | Vendedora Senior | 4 |
Usamos LEFT JOIN para incluir también a los empleados que no gestionaron ningún pedido en diciembre. Los directivos y el personal de almacén aparecen con 0 porque no gestionan pedidos directamente.
HAVING COUNT con DISTINCT
Puedes combinar COUNT(DISTINCT ...) con HAVING para contar valores únicos dentro de cada grupo:
SELECT
e.nombre AS empleado,
COUNT(DISTINCT p.cliente_id) AS clientes_atendidos
FROM pedidos p
JOIN empleados e ON p.empleado_id = e.id
GROUP BY e.id, e.nombre
HAVING clientes_atendidos >= 5
ORDER BY clientes_atendidos DESC;| empleado | clientes_atendidos |
|---|---|
| Natalia | 8 |
| Daniel | 6 |
| Patricia | 5 |
Natalia ha atendido a 8 clientes diferentes, Daniel a 6 y Patricia a 5. COUNT(DISTINCT p.cliente_id) cuenta clientes únicos, ignorando si un mismo cliente hizo varios pedidos con el mismo empleado.
HAVING con comparaciones entre agregaciones
Puedes usar expresiones complejas en HAVING, incluyendo comparaciones entre funciones de agregación:
SELECT
c.nombre AS categoria,
MIN(p.precio) AS precio_min,
MAX(p.precio) AS precio_max,
MAX(p.precio) - MIN(p.precio) AS rango_precios
FROM productos p
JOIN categorias c ON p.categoria_id = c.id
GROUP BY c.nombre
HAVING rango_precios > 100
ORDER BY rango_precios DESC;| categoria | precio_min | precio_max | rango_precios |
|---|---|---|---|
| Smartphones | 599.99 | 1299.99 | 700.00 |
| Portátiles | 1399.00 | 1899.99 | 500.99 |
| Muebles | 149.99 | 599.00 | 449.01 |
| Cocina | 39.99 | 249.99 | 210.00 |
| Fitness | 19.99 | 199.99 | 180.00 |
Categorías donde la diferencia entre el producto más caro y el más barato supera los 100 euros. Los smartphones tienen el mayor rango de precios (700 euros de diferencia).
Practica con HAVING COUNT
Usa el editor para filtrar grupos por cantidad de elementos:
En el siguiente artículo veremos ROLLUP, que permite añadir subtotales y totales generales a las consultas con GROUP BY.
Escrito por Eduardo Lázaro
