SQL · Guías · 30 de octubre de 2026 · 11 min de lectura

GROUP BY y HAVING en SQL: diferencia entre WHERE y HAVING, con ejemplos

Maletas en cintas transportadoras que un embudo separa en grupos

GROUP BY y HAVING no son alternativas: hacen dos trabajos distintos en la misma consulta. GROUP BY agrupa filas (una fila de salida por aeropuerto, por día, por clase) para que los agregados como COUNT y AVG se calculen por grupo. HAVING filtra esos grupos cuando los agregados ya están calculados. WHERE es la tercera pieza: filtra filas individuales antes de agrupar. La regla corta: una condición sobre una columna tal cual va en WHERE; una condición sobre un agregado (COUNT(*) > 1000, AVG(amount) > 300) va en HAVING.

Todos los ejemplos usan la base de datos de una aerolínea del curso de SQL de Nomto: 33.121 vuelos entre 104 aeropuertos en el verano de 2026, con reservas, billetes y tramos de vuelo (valores en inglés, dinero en dólares). Puedes pegar cada consulta en la sala de SQL de Nomto y comprobar el resultado tú mismo.

Qué hace GROUP BY

Sin GROUP BY, un agregado convierte toda la tabla en una sola fila. Con él, obtienes una fila por cada valor distinto de las columnas de agrupación.

SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights
Arrived 16707
Scheduled 15383
On Time 518
Cancelled 414
Departed 58
Delayed 41

Seis estados, seis filas. Cada fila es un grupo, y COUNT(*) se ha calculado por separado dentro de cada uno.

Se puede agrupar por varias columnas y por expresiones. Aquí, el número de asientos por modelo de avión, con un conteo condicional de asientos de clase business en la misma pasada:

SELECT aircraft_code,
       COUNT(*) AS seats,
       SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) AS business
FROM seats
GROUP BY aircraft_code
ORDER BY seats DESC;
aircraft_code seats business
773 402 30
763 222 30
321 170 28
320 140 20
733 130 12
319 116 20
SU9 97 12
CR2 50 0
CN1 12 0

Qué hace HAVING

HAVING conserva o descarta grupos enteros. Se evalúa cuando los agregados ya existen, así que puede usarlos.

SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE status = 'Arrived'
GROUP BY departure_airport
HAVING COUNT(*) > 1000
ORDER BY departures DESC;
departure_airport departures
DME 1618
SVO 1503

Léela en el orden en que lo hace la base de datos: toma todos los vuelos, quédate solo con los que llegaron (WHERE), agrúpalos por aeropuerto de salida, cuenta cada grupo y conserva solo los grupos con más de 1.000 filas (HAVING). Pasan dos aeropuertos de Moscú, Domodédovo y Sheremétievo.

HAVING puede usar cualquier agregado, no solo el que seleccionas. ¿Qué modelos de avión no tienen ningún asiento de business?

SELECT aircraft_code, COUNT(*) AS seats
FROM seats
GROUP BY aircraft_code
HAVING SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) = 0;
aircraft_code seats
CR2 50
CN1 12

El Bombardier CRJ-200 y el Cessna 208 Caravan.

Diferencia entre WHERE y HAVING

WHERE HAVING
Filtra filas individuales grupos
Se ejecuta antes de GROUP BY después de GROUP BY y de los agregados
Puede usar agregados no sí
Puede usar columnas sin agregar cualquier columna solo las agrupadas (o dentro de agregados)
Funciona sin GROUP BY sí sí, todo el resultado es un único grupo

La consecuencia práctica: una condición en WHERE cambia lo que entra en los agregados; una condición en HAVING, no. Compara estas dos consultas sobre precios de tramos.

Precio medio por clase, quedándote solo con los tramos que cuestan más de 300 dólares:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
WHERE amount > 300
GROUP BY fare_conditions
ORDER BY avg_price DESC;
fare_conditions segments avg_price
Business 63467 739.73
Economy 90837 500.99
Comfort 6919 474.92

Precio medio por clase, quedándote solo con las clases cuya media supera los 300 dólares:

SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
GROUP BY fare_conditions
HAVING AVG(amount) > 300
ORDER BY avg_price DESC;
fare_conditions segments avg_price
Business 107642 511.43
Comfort 17291 327.41

La primera consulta responde a «cuánto cuesta de media un tramo caro en cada clase»: todas las medias están infladas porque se han tirado los tramos baratos, y Economy incluso adelanta a Comfort. La segunda responde a «qué clases son caras de media»: las medias son las reales (la de Economy es 159,60 dólares, por eso queda fuera). Mismas columnas, mismo número, preguntas completamente distintas.

Si sirven los dos, usa WHERE

Una condición sobre una columna agrupada técnicamente puede ir en cualquiera de los dos sitios:

SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO')
GROUP BY departure_airport
ORDER BY departures DESC;
departure_airport departures
DME 3217
SVO 2981
VKO 1719

Si mueves la condición a HAVING departure_airport IN ('SVO', 'DME', 'VKO'), salen las mismas tres filas. Pero la versión con HAVING primero agrupa y cuenta los 104 aeropuertos y luego descarta 101 grupos. Muchos optimizadores bajan esa condición por ti, pero no conviene contar con ello, y WHERE además le dice al lector tu intención: esas filas quedan fuera desde el principio.

En qué orden ejecuta SQL una consulta

Escribes SELECT primero, pero la base de datos evalúa las cláusulas en este orden lógico:

  1. FROM y JOIN: forma el conjunto de filas
  2. WHERE: descarta filas
  3. GROUP BY: forma los grupos
  4. se calculan los agregados de cada grupo
  5. HAVING: descarta grupos
  6. SELECT: calcula las columnas de salida y los alias
  7. DISTINCT
  8. ORDER BY
  9. LIMIT / OFFSET

Casi todos los errores con GROUP BY y HAVING salen de esta lista:

Errores frecuentes

1. Un agregado en WHERE

SELECT departure_airport, COUNT(*)
FROM flights
WHERE COUNT(*) > 1000
GROUP BY departure_airport;

Falla. PostgreSQL responde que las funciones de agregado no están permitidas en WHERE («aggregate functions are not allowed in WHERE»). Lleva la condición a HAVING.

2. Una columna seleccionada que ni se agrupa ni se agrega

SELECT departure_airport, arrival_airport, COUNT(*)
FROM flights
GROUP BY departure_airport;

También falla: la columna arrival_airport debe aparecer en GROUP BY o usarse dentro de una función de agregado. Un aeropuerto de salida tiene muchos aeropuertos de llegada, así que la base de datos no puede elegir un único valor por grupo. O añades arrival_airport al GROUP BY (una fila por ruta) o lo agregas, por ejemplo con COUNT(DISTINCT arrival_airport).

PostgreSQL hace una excepción: si agrupas por la clave primaria de una tabla, puedes seleccionar otras columnas de esa tabla. Agrupar por b.book_ref te deja seleccionar b.total_amount. Poner las dos en GROUP BY funciona en todas partes, así que las consultas de esta guía lo hacen así:

SELECT b.book_ref, COUNT(*) AS tickets, b.total_amount
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref
GROUP BY b.book_ref, b.total_amount
HAVING COUNT(*) >= 5
ORDER BY b.total_amount DESC
LIMIT 5;
book_ref tickets total_amount
65A6EA 5 10656.00
7734A5 5 8291.00
148C46 5 8135.00
070682 5 8012.00
8DA112 5 6150.00

En toda la base de datos solo hay 23 reservas con cinco billetes o más.

3. Comparar grupos pequeños con grupos grandes

Retraso medio de salida por aeropuerto, entre los vuelos que realmente despegaron:

SELECT departure_airport,
       ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min,
       COUNT(*) AS departed
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
ORDER BY avg_delay_min DESC
LIMIT 3;
departure_airport avg_delay_min departed
ULY 24.4 97
BQS 22.8 31
LPK 22.7 22

Los «peores» aeropuertos tienen entre 22 y 97 salidas, así que un par de retrasos largos dominan su media. Un tamaño mínimo de grupo en HAVING hace la comparación más justa:

SELECT departure_airport,
       COUNT(*) AS departed,
       ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC
LIMIT 5;
departure_airport departed avg_delay_min
SVX 348 14.7
VKO 870 13.4
BZK 306 13.2
KJA 358 13.1
LED 960 12.6

Fíjate en el reparto de tareas: WHERE decide qué vuelos cuentan como despegados y HAVING decide qué aeropuertos tienen suficientes.

4. COUNT(*) cuando querías COUNT(columna)

COUNT(*) cuenta filas; COUNT(columna) cuenta valores que no son NULL. En una consulta agrupada esa diferencia suele ser justo lo que importa:

SELECT status, COUNT(*) AS flights, COUNT(actual_departure) AS with_departure
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights with_departure
Arrived 16707 16707
Scheduled 15383 0
On Time 518 0
Cancelled 414 8
Departed 58 58
Delayed 41 0

Ocho vuelos cancelados tienen una hora de salida registrada, una rareza de los datos que con COUNT(*) solo nunca verías. Un HAVING COUNT(actual_departure) > 0 en esta consulta dejaría tres grupos, no seis.

5. Agrupar por una expresión en un sitio y no en el otro

Si agrupas por fecha, agrupa por la misma expresión que seleccionas:

SELECT CAST(scheduled_departure AS date) AS day, COUNT(*) AS cancelled
FROM flights
WHERE status = 'Cancelled'
GROUP BY CAST(scheduled_departure AS date)
HAVING COUNT(*) >= 15
ORDER BY day;
day cancelled
2026-09-07 18
2026-09-08 21
2026-09-09 51
2026-09-10 287

Agrupar por la marca de tiempo scheduled_departure tal cual daría un grupo por cada minuto de salida, y el HAVING no encontraría nada. (La acumulación al final del calendario merece una segunda mirada antes de que alguien la presente como una crisis: 403 de las 414 cancelaciones están en la programación futura de septiembre).

HAVING sin GROUP BY

Si una consulta tiene agregados y HAVING pero no GROUP BY, todo el resultado se trata como un único grupo. La consulta devuelve una fila o ninguna:

SELECT COUNT(*) AS flights
FROM flights
HAVING COUNT(*) > 30000;
flights
33121

Cambia el umbral a 40.000 y obtendrás cero filas. De vez en cuando es útil en controles de calidad de datos: una consulta que devuelve una fila solo cuando algo va mal.

Lista rápida para decidir

Si hay JOIN de por medio, comprueba que las filas no se han multiplicado antes de fiarte de un SUM o un COUNT; lo explica la guía de JOIN en SQL. Cuando necesitas un agregado junto a cada fila en lugar de juntar filas, lo que buscas son las funciones de ventana, no GROUP BY. Y si quieres repasar SELECT, WHERE y ORDER BY desde el principio, empieza por las consultas SQL básicas.

Preguntas frecuentes

¿Se puede usar HAVING sin GROUP BY?

Sí. Todo el resultado se convierte en un único grupo y la consulta devuelve una fila o nada, según la condición de HAVING.

¿HAVING es más lento que WHERE?

En las condiciones sobre agregados no hay elección, así que la velocidad no es la cuestión. En las condiciones sobre columnas sin agregar, WHERE permite descartar filas antes de agrupar, lo que nunca es más lento y a menudo es más rápido. Usa WHERE siempre que la condición no necesite un agregado.

¿Puedo usar un alias de columna en HAVING?

No en PostgreSQL ni en SQL estándar: HAVING se evalúa antes que SELECT, así que el alias todavía no existe. Repite la expresión. MySQL y otros motores permiten el alias como extensión.

¿Se pueden usar WHERE y HAVING en la misma consulta?

Sí, y es lo habitual: WHERE elige las filas que alimentan los agregados y HAVING elige los grupos que se conservan. Los ejemplos de salidas y de retrasos de arriba usan los dos.

¿Qué diferencia hay entre GROUP BY y DISTINCT?

SELECT DISTINCT status FROM flights y SELECT status FROM flights GROUP BY status devuelven los mismos seis valores. GROUP BY además permite calcular agregados por grupo y filtrar grupos con HAVING; DISTINCT solo elimina filas duplicadas.

También en Português (Brasil), Tiếng Việt, English, 简体中文