1. Qué es
Los dos filtran. La diferencia cabe en una línea:
WHEREfiltra filas antes de agrupar.HAVINGfiltra grupos después de agrupar.
No son alternativas de estilo: filtran cosas distintas en momentos distintos. WHERE es el paso 2 del orden-logico-de-ejecucion y HAVING el 4, con el GROUP BY en medio.
WHERE |
HAVING |
|
|---|---|---|
| Cuándo | Antes de agrupar | Después de agrupar |
| Qué descarta | Filas | Grupos enteros |
| Puede usar agregaciones | ❌ No | ✅ Sí |
| Puede usar columnas de la tabla | ✅ Todas | Solo las agrupadas |
Puede usar alias del SELECT |
❌ No | ❌ No |
Sin GROUP BY |
Filtra filas | Filtra la fila única del resultado |
2. Sintaxis
SELECT columna, agregación(...)
FROM tabla
WHERE condición_sobre_filas
GROUP BY columna
HAVING condición_sobre_grupos
ORDER BY ...;
El orden de escritura es obligatorio: WHERE antes del GROUP BY, HAVING después.
3. Ejemplos sobre Northwind
HAVING en su uso natural — filtrar por una agregación:
SELECT customer_id, count(*) AS pedidos
FROM orders
GROUP BY customer_id
HAVING count(*) >= 20
ORDER BY pedidos DESC;
customer_id | pedidos
-------------+---------
SAVEA | 31
ERNSH | 30
QUICK | 28
Tres clientes de 89 superan los 20 pedidos. Esto no se puede escribir con WHERE: cuando el WHERE se evalúa, no hay grupos y no hay nada que contar.
SELECT customer_id, count(*) FROM orders WHERE count(*) >= 20 GROUP BY customer_id;
ERROR: aggregate functions are not allowed in WHERE
El error es preciso y hay que leerlo como una explicación, no como un obstáculo.
3.1 HAVING tampoco acepta el alias
SELECT customer_id, count(*) AS pedidos
FROM orders
GROUP BY customer_id
HAVING pedidos >= 20;
ERROR: column "pedidos" does not exist
GROUP BY sí acepta el alias, HAVING no. Es la asimetría del orden-logico-de-ejecucion: la comodidad del alias se concede a GROUP BY y ORDER BY, y a nadie más. En HAVING hay que repetir la agregación entera.
3.2 Los dos juntos, cada uno en su sitio
SELECT ship_country,
count(*) AS pedidos,
round(avg(freight),2) AS flete_medio
FROM orders
WHERE order_date >= '1997-01-01' AND order_date < '1998-01-01'
GROUP BY ship_country
HAVING count(*) > 20 AND avg(freight) > 80
ORDER BY flete_medio DESC;
ship_country | pedidos | flete_medio
--------------+---------+-------------
Austria | 21 | 178.36
Germany | 64 | 97.38
USA | 60 | 96.99
Léelo como una frase: "de los pedidos de 1997 (WHERE), agrupados por país, quédate con los países que tienen más de 20 pedidos y flete medio superior a 80 (HAVING)".
Austria es el hallazgo: 21 pedidos —justo por encima del corte— y un flete medio de 178, casi el doble que Alemania. Con 21 pedidos apenas, esa media es frágil; el HAVING count(*) > 20 está precisamente para eso, para no sacar conclusiones de grupos de tres filas.
3.3 El caso peligroso: cuando los dos "funcionan"
Estas dos consultas devuelven lo mismo (83 pedidos de Brasil):
SELECT ship_country, count(*) FROM orders WHERE ship_country = 'Brazil' GROUP BY ship_country;
SELECT ship_country, count(*) FROM orders GROUP BY ship_country HAVING ship_country = 'Brazil';
Filtrar por una columna agrupada se puede poner en cualquiera de los dos sitios, y PostgreSQL genera el mismo plan para ambas: empuja el filtro hacia abajo por su cuenta, aunque escribieras HAVING. Puede hacerlo porque el resultado es demostrablemente el mismo — el orden de ejecución es lógico, no físico. Se comprueba con EXPLAIN, al final del curso (explain-y-explain-analyze).
Entonces, ¿da igual? No, por dos razones que no son de rendimiento:
- Se lee distinto.
WHEREdice "solo me interesan las filas de Brasil".HAVINGdice "agrupa todo y luego tira 20 países". La primera es lo que querías. - El optimizador ayuda con los casos evidentes, no con todos. Empujar un
= 'Brazil'es fácil. UnHAVINGsobre una expresión más rara puede quedarse donde lo escribiste, y entonces sí pagas por agrupar de más.
Regla: si la condición no menciona una agregación, va en
WHERE. Sin excepciones.
3.4 WHERE y HAVING dan resultados distintos cuando no filtras lo mismo
Este es el punto que separa entender de memorizar. Categorías con 10 o más productos:
-- A: categorías con 10+ productos ACTIVOS
SELECT category_id, count(*) FROM products
WHERE NOT discontinued
GROUP BY category_id HAVING count(*) >= 10 ORDER BY 2 DESC;
category_id | count
-------------+-------
3 | 13
8 | 12
2 | 11
4 | 10
-- B: categorías SIN NINGÚN producto descatalogado
SELECT category_id, count(*) FROM products
GROUP BY category_id HAVING NOT bool_or(discontinued);
category_id | count
-------------+-------
8 | 12
4 | 10
3 | 13
No son la misma pregunta y no dan la misma respuesta. La A cuenta productos activos y admite categorías que además tienen descatalogados; la B exige que la categoría entera esté limpia. La categoría 2 aparece en A y no en B: tiene 11 activos y al menos uno de baja.
WHERE decide qué filas entran en el grupo. HAVING decide qué grupos sobreviven. Cambiar una condición de sitio no es refactorizar: es cambiar la pregunta.
3.5 Encontrar duplicados, que no es lo mismo que quitarlos
DISTINCT los esconde (distinct-y-duplicados). Para verlos se usa GROUP BY + HAVING:
SELECT
customer_id,
order_date,
count(*)
FROM
orders
GROUP BY
customer_id,
order_date
HAVING
count(*) > 1
ORDER BY
count(*) DESC;
customer_id | order_date | count
-------------+------------+-------
LACOR | 1998-03-24 | 2
LINOD | 1998-01-19 | 2
SAVEA | 1997-10-22 | 2
7 casos de un cliente con dos pedidos el mismo día. ¿Son duplicados? Vamos a mirar:
SELECT
order_id,
customer_id,
order_date,
employee_id,
freight
FROM
orders
WHERE
customer_id = 'LACOR'
AND order_date = '1998-03-24';
order_id | customer_id | order_date | employee_id | freight
----------+-------------+------------+-------------+---------
10972 | LACOR | 1998-03-24 | 4 | 0.02
10973 | LACOR | 1998-03-24 | 6 | 15.17
No son duplicados. Distinto id, distinto empleado, distinto flete: son dos pedidos reales del mismo cliente el mismo día. Y esa es la lección del ejercicio:
GROUP BY ... HAVING count(*) > 1no encuentra duplicados. Encuentra repeticiones de la clave que tú elegiste. Si eliges mal la clave, todo parece duplicado. Decidir qué combinación de columnas debería ser única es una pregunta de negocio, no de SQL.
3.6 HAVING sin GROUP BY
Es legal y raro. Sin GROUP BY, toda la tabla es un grupo, así que HAVING filtra esa única fila:
SELECT count(*) AS total FROM orders HAVING count(*) > 100; -- 830
SELECT count(*) AS total FROM orders HAVING count(*) > 10000; -- 0 filas
La segunda no devuelve 0: devuelve cero filas. Es la diferencia entre "el resultado es cero" y "no hay resultado", y en un informe se ve como una celda vacía en vez de un cero. Se usa poco, pero sirve para una cosa: una consulta que solo devuelve algo si se cumple una condición global — por ejemplo, una alerta que se dispara solo si el total supera un umbral.
4. Dónde encaja en el orden de ejecución
Con HAVING ya tienes las seis cláusulas básicas del orden-logico-de-ejecucion. Es el primer punto del curso donde se puede ver el orden entero, funcionando.
El embudo, medido de verdad. Es el mismo recorrido que hiciste en orden-logico-de-ejecucion, ahora con los números de cada paso. Países con más de 10 pedidos caros:
SELECT ship_country AS pais,
count(*) AS pedidos
FROM orders
WHERE freight > 50
GROUP BY pais
HAVING count(*) > 10
ORDER BY pedidos DESC;
pais | pedidos
---------+---------
USA | 61
Germany | 58
Austria | 33
Brazil | 32
France | 27
...
(las cinco primeras de las diez que devuelve)
Paso a paso, y cada número se puede comprobar por separado:
| Paso | Qué queda |
|---|---|
1. FROM orders |
830 filas |
2. WHERE freight > 50 |
360 filas |
3. GROUP BY pais |
21 grupos |
4. HAVING count(*) > 10 |
10 grupos |
5-6. SELECT y ORDER BY |
10 filas, ordenadas |
SELECT count(*) FROM orders; -- 830
SELECT count(*) FROM orders WHERE freight > 50; -- 360
Por qué el WHERE no puede contar. Este es el error que manda a todo el mundo a buscar HAVING por primera vez:
SELECT ship_country, count(*)
FROM orders
WHERE count(*) > 10
GROUP BY ship_country;
ERROR: aggregate functions are not allowed in WHERE
PostgreSQL es explícito, y tiene razón: el WHERE es el paso 2 y el GROUP BY el 3. Cuando se evalúa el WHERE, cada fila está sola y no pertenece a ningún grupo, así que no hay nada que contar. Para eso existe HAVING, que corre en el paso 4, con los grupos ya formados.
Y por qué HAVING no acepta el alias, aunque GROUP BY sí. La asimetría que sorprende a todo el mundo:
SELECT ship_country AS pais, count(*) AS pedidos
FROM orders
GROUP BY pais -- ✅ funciona
HAVING pedidos > 10; -- ❌ ERROR: column "pedidos" does not exist
No es que HAVING sea más estricto por capricho: ninguno de los dos debería ver el alias, porque nace en el paso 5. Lo que pasa es que el estándar SQL concede esa comodidad a GROUP BY y a ORDER BY, y no a WHERE ni a HAVING. En HAVING hay que repetir la agregación entera: HAVING count(*) > 10.
5. Errores comunes
- Poner una agregación en el
WHERE. Error explícito. Va enHAVING. - Usar el alias del
SELECTenHAVING. Error. Repite la agregación. - Poner en
HAVINGun filtro que no menciona agregaciones. Funciona, se lee peor, y no siempre lo arregla el optimizador. - Creer que
WHEREyHAVINGson intercambiables. Lo son solo cuando filtras por una columna agrupada. En cuanto la condición toca una columna no agrupada, cambian la respuesta. - Filtrar en
HAVINGpor una columna que no está en elGROUP BY. Da error: no existe a nivel de grupo. - Olvidar
HAVING count(*) > nen análisis de medias. Una media sobre dos filas no es una media, es una anécdota. ElHAVINGes lo que evita publicarla. - Esperar un
0dondeHAVINGdevuelve cero filas.
6. Diferencias con SQL Server
WHERE y HAVING funcionan igual en los dos motores: mismas reglas, mismo orden, mismos errores. Es de lo más portable que hay.
La única diferencia hereda la del tema anterior: PostgreSQL permite GROUP BY por alias o posición y SQL Server no, así que una consulta escrita como GROUP BY 1 ... HAVING count(*) > 20 hay que reescribirla con la expresión completa al migrar. HAVING en sí no cambia.
Y una nota de estilo que vale para ambos: en SQL Server es frecuente ver HAVING usado como filtro general porque "así se filtra al final". Es la misma mala costumbre en los dos motores.
7. Ejercicios
- Los países con más de 50 pedidos.
- Los productos vendidos en más de 50 líneas de pedido distintas, con las unidades totales.
- Los clientes cuyo flete medio supera los 100, pero solo si tienen al menos 10 pedidos.
- Las categorías donde el precio medio de los productos activos supera los 30.
- Explica la diferencia entre estas dos y di cuál responde a "empleados que vendieron algo en 1998": ```sql SELECT employee_id, count(*) FROM orders WHERE extract(year FROM order_date) = 1998 GROUP BY employee_id;
SELECT employee_id, count() FROM orders
GROUP BY employee_id HAVING max(order_date) >= '1998-01-01';
6. Arregla esta consulta:sql
SELECT customer_id, count() FROM orders WHERE count() > 20 GROUP BY customer_id;
```
7. Escribe la consulta que responde: empleados que gestionaron más de 100 pedidos, del que más al que menos.* Usa las seis cláusulas básicas menos una — la que aquí no hace falta — y di cuál has dejado fuera y por qué.
8. Pregunta de negocio
Marketing quiere lanzar un programa de fidelización para "clientes buenos": los que compran a menudo y gastan bien. Piden la lista. No te dan umbrales — dicen que los pongas tú.
La parte difícil no es el SQL. Es defender los números que elijas.