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;
ciudadtotal_clientes
Madrid2
Barcelona2
Valencia2
Zaragoza2

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;
etiquetaveces_usada
básico1
deporte1
ergonómico1
fitness1
gaming1

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_pedidosnum_clientes
113
25
31

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;
nombrenum_resenasmedia
iPhone 15 Pro24.5
Camiseta algodón básica23.5
Zapatillas running pro24.5
Clean Code24.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;
nombrepuestopedidos_gestionados
RicardoDirector General0
AlbertoDirector de Logística0
RaúlVendedor Junior0
CristinaResponsable de Almacén0
MarcosOperario de Almacén0
InésOperaria de Almacén0
SofíaDirectora de Ventas0
PatriciaVendedora4
DanielVendedor4
NataliaVendedora Senior4

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;
empleadoclientes_atendidos
Natalia8
Daniel6
Patricia5

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;
categoriaprecio_minprecio_maxrango_precios
Smartphones599.991299.99700.00
Portátiles1399.001899.99500.99
Muebles149.99599.00449.01
Cocina39.99249.99210.00
Fitness19.99199.99180.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:

Simulador SQL
Ctrl+Enter para ejecutar

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