La diferencia entre WHERE y HAVING no es de estilo: es de en qué momento actúa cada una dentro del orden en que un motor evalúa una consulta. WHERE filtra filas individuales antes de que existan agrupaciones, así que solo puede mirar columnas de las tablas. HAVING filtra grupos después de calcular GROUP BY y sus funciones de agregación, así que puede comparar contra un COUNT, un SUM o un AVG. De ese único hecho —el orden de evaluación— salen todas las reglas prácticas: por qué WHERE no acepta agregaciones, por qué HAVING funciona incluso sin GROUP BY, y por qué WHERE suele rendir mejor.
Ver índice de contenidos
- 01El orden lógico de evaluación de una consulta
- 02WHERE: filtra filas antes de que existan grupos
- 03HAVING: filtra grupos después de calcularlos
- 04HAVING sin GROUP BY: la tabla entera como un grupo
- 05WHERE vs. HAVING: tabla comparativa
- 06Por qué WHERE puede usar índices y HAVING no
- 07La extensión de MySQL: alias del SELECT en HAVING
- 08Un modelo lógico, no el plan de ejecución real
- 09Errores frecuentes
- 10Preguntas frecuentes
- 11Fuentes
El orden lógico de evaluación de una consulta
Antes de comparar WHERE con HAVING conviene entender algo que rara vez se explica: una consulta SQL no se ejecuta en el orden en que se escribe, sino en un orden lógico fijo. Microsoft lo documenta de forma numerada para Transact-SQL, y ese orden —con matices menores— aparece también en PostgreSQL y en SQLite:
- FROM / JOINArma el conjunto de filas de partida, combinando las tablas que participan.
- WHEREFiltra esas filas una por una, antes de que exista ningún grupo.
- GROUP BYJunta en un grupo las filas que comparten un mismo valor.
- HAVINGFiltra los grupos ya formados, según el resultado de una función de agregación.
- SELECTRecién acá se calculan las columnas del resultado y se asignan los alias.
- DISTINCTElimina filas duplicadas del resultado que ya calculó el SELECT.
- ORDER BYOrdena ese resultado final, y por eso sí puede usar los alias creados en el SELECT.
- LIMIT(o TOP, o FETCH FIRST según el motor) recorta la cantidad de filas que se devuelven.
Ese orden explica casi todas las reglas de esta guía. WHERE aparece segundo, cuando todavía no existe ningún grupo ni valor agregado: por eso no puede filtrar por un COUNT, SUM o AVG. HAVING aparece después de GROUP BY, con los grupos ya resueltos: por eso sí puede usarlos, incluso sin GROUP BY explícito, como vas a ver.
WHERE: filtra filas antes de que existan grupos
Imaginá una tabla productos con columnas nombre, categoria, stock y descontinuado. Si necesitás ver qué productos activos tienen el stock por debajo de un umbral crítico, la condición se aplica sobre cada fila individual, antes de agrupar nada:
SELECT nombre, categoria, stock
FROM productos
WHERE stock < 5
AND descontinuado = false;
En este punto —paso 2 del orden lógico— no existe ningún grupo ni ningún total: la base de datos todavía decide, fila por fila, cuáles conservar. Por eso WHERE COUNT(*) > 3 no tiene sentido y el motor lo rechaza con un error: COUNT(*) se calcula por grupo, y cuando corre WHERE los grupos ni siquiera existen todavía.
Esto no cambia si después agregás un GROUP BY. Esta consulta cuenta cuántos productos con stock crítico hay por categoría:
SELECT categoria, COUNT(*) AS productos_criticos
FROM productos
WHERE stock < 5 -- filtra filas: solo productos con stock bajo
GROUP BY categoria; -- arma un grupo por categoría
El WHERE sigue actuando sobre filas individuales, igual que antes; solo cambió que las filas sobrevivientes se agrupan por categoría. No hay ningún HAVING, y no hace falta: no filtramos por el COUNT, solo lo mostramos.
HAVING: filtra grupos después de calcularlos
Ahora sumá una condición que sí depende del valor agregado: quedarte solo con las categorías que tengan más de tres productos críticos. Ese filtro no puede ir en WHERE, porque ese número todavía no existe cuando WHERE se ejecuta. Va en HAVING, que corre después de GROUP BY:
SELECT categoria, COUNT(*) AS productos_criticos
FROM productos
WHERE stock < 5
GROUP BY categoria
HAVING COUNT(*) > 3;
Otro ejemplo, con una tabla pedidos (columnas cliente_id, monto, estado), deja ver la misma lógica combinando las dos cláusulas en una sola consulta con propósitos distintos:
SELECT cliente_id, SUM(monto) AS total_comprado
FROM pedidos
WHERE estado = 'confirmado' -- 1. filtra filas: solo pedidos confirmados
GROUP BY cliente_id -- 2. arma un grupo por cliente
HAVING SUM(monto) >= 50000; -- 3. filtra grupos: gastaron 50.000 o más
El WHERE decide qué pedidos entran —los confirmados—. El HAVING decide qué clientes aparecen, según el total ya calculado. Ninguno reemplaza al otro: mover estado al HAVING funcionaría, pero sería más costoso, porque el motor agruparía pedidos que después descarta; y mover el monto al WHERE fallaría directo, porque SUM(monto) todavía no existe ahí.
HAVING sin GROUP BY: la tabla entera como un grupo
Un detalle que casi ningún tutorial explica: HAVING no necesita un GROUP BY para ser válido. Si lo usás sin agrupar, toda la tabla —o lo que quedó después de aplicar WHERE— se trata como un único grupo implícito, y la condición se aplica sobre el agregado de ese grupo completo.
SELECT SUM(monto) AS total_facturado
FROM pedidos
WHERE estado = 'confirmado'
HAVING SUM(monto) > 1000000;
Esta consulta calcula un solo total —la tabla entera cuenta como un grupo— y HAVING decide si ese único resultado se muestra o no. Si el total supera el millón, devuelve una fila con ese valor; si no lo supera, devuelve cero filas, aunque haya pedidos confirmados en la tabla.
Es una forma poco frecuente de usar HAVING, pero válida, y sirve para preguntas del tipo «¿este total llegó al umbral que me interesa?», sin necesitar una subconsulta.
WHERE vs. HAVING: tabla comparativa
Todo lo anterior se puede resumir en siete puntos de comparación directa.
| Aspecto | WHERE | HAVING |
|---|---|---|
| Cuándo se evalúa | Paso 2: después de FROM/JOIN, antes de agrupar | Paso 4: después de GROUP BY y de calcular las agregaciones |
| Qué filtra | Filas individuales de las tablas | Grupos ya formados, según su resultado agregado |
| Funciones de agregación | No puede usarlas | Es su terreno natural: COUNT, SUM, AVG, MIN, MAX |
| Necesita GROUP BY | No tiene relación con GROUP BY | No: sin él, trata toda la tabla como un solo grupo |
| Uso de índices | Puede aprovechar un índice sobre la columna filtrada | No actúa directamente sobre columnas indexadas |
| Costo relativo | Descarta filas temprano, antes de agrupar | Actúa sobre un resultado ya calculado, después de agrupar todo lo que pasó el WHERE |
| Alias del SELECT | No, en ningún motor estándar | En MySQL sí (extensión no estándar); en otros motores puede fallar |
Por qué WHERE puede usar índices y HAVING no
La tabla anterior resume la diferencia, pero vale entender por qué el uso de índices —estructuras que el motor usa para localizar filas sin recorrer la tabla completa, ver la guía de índices— se reparte de forma tan distinta.
WHERE actúa en el paso 2, sobre columnas reales de la tabla. Si existe un índice sobre stock o estado, el motor puede usarlo para localizar directamente las filas que cumplen la condición, sin leer las demás. Cuantas menos sobrevivan, menos hay que agrupar después.
HAVING actúa en el paso 4, sobre un valor que el motor recién terminó de calcular por cada grupo. Ese valor no es una columna almacenada ni indexada: es el resultado de un cómputo hecho durante el GROUP BY. Por eso HAVING no puede aprovechar un índice para saltarse grupos: el motor tiene que agrupar y agregar todo lo que pasó el WHERE antes de decidir qué grupos conservar.
Toda condición que se pueda expresar sobre una columna de la tabla, sin depender de una agregación, conviene escribirla en WHERE. No es una preferencia de estilo: es la diferencia entre descartar datos antes de hacer el trabajo pesado de agrupar, o después.
La extensión de MySQL: alias del SELECT en HAVING
Hay un detalle de sintaxis que conviene conocer, porque en un motor funciona y en otro puede fallar. MySQL permite que HAVING haga referencia a un alias definido en el SELECT:
-- Válido en MySQL: HAVING referencia un alias del SELECT
SELECT categoria, COUNT(*) AS productos_criticos
FROM productos
WHERE stock < 5
GROUP BY categoria
HAVING productos_criticos > 3;
La documentación de MySQL sobre GROUP BY y HAVING confirma esto como una extensión propia, que el estándar no exige y otros motores no garantizan. Para portabilidad, la forma que funciona en cualquier motor es repetir la expresión completa:
-- Portable: repite la expresión completa en lugar del alias
SELECT categoria, COUNT(*) AS productos_criticos
FROM productos
WHERE stock < 5
GROUP BY categoria
HAVING COUNT(*) > 3;
Es la misma situación que con los alias en WHERE: HAVING corre en el paso 4 y SELECT recién en el 5, así que en el estándar el alias todavía no debería existir. MySQL lo resuelve porque procesa la consulta de un modo que se lo permite, pero es una comodidad de ese motor, no una garantía del lenguaje.
Un modelo lógico, no el plan de ejecución real
Todo lo anterior describe un orden lógico: el modelo que usás para razonar qué es válido escribir y qué no. No es, necesariamente, la secuencia exacta de pasos que la base de datos ejecuta por dentro.
La documentación de SQLite lo dice de forma explícita: el orden de procesamiento que describe para un SELECT es «puramente ilustrativo», y el optimizador real puede reordenar las operaciones si el resultado final es equivalente al que produciría ese orden lógico. Por eso un plan de ejecución real puede aplicar una condición de WHERE en un momento distinto del que sugiere la lista, si eso le permite descartar filas antes y llegar al mismo resultado con menos trabajo.
El orden lógico sigue siendo la referencia correcta para saber qué puede filtrar cada cláusula y por qué. Pero conviene tenerlo presente antes de sacar conclusiones de rendimiento a partir de cómo se escriben las cláusulas: la forma confiable de saber qué hace un motor con una consulta concreta es revisar su plan de ejecución, no asumirlo del modelo lógico.
Errores frecuentes
- Creer que WHERE y HAVING son intercambiables. Cada una filtra en un momento distinto, con información distinta disponible.
- Pensar que HAVING siempre necesita un GROUP BY explícito. Sin él, trata la tabla entera como un solo grupo.
- Suponer que el orden en que se escribe la consulta es el orden en que se ejecuta. El SELECT se lee primero pero se calcula en el paso 5.
- Usar HAVING para filtrar filas individuales que ya se podrían descartar en WHERE. Obliga a agrupar de más antes de descartar.
- Asumir que un alias del SELECT funciona en HAVING en cualquier motor. Es una extensión de MySQL, no una garantía del estándar.
- Confundir el modelo lógico con el plan de ejecución físico real. Uno explica la sintaxis; el otro, el rendimiento real.
Preguntas frecuentes
¿Por qué no puedo usar una función de agregación en WHERE?
Porque WHERE se evalúa en el paso 2 del orden lógico, justo después de FROM y los JOIN, cuando todavía no existe ningún grupo ni valor agregado. Una función como COUNT, SUM o AVG necesita filas ya agrupadas —aunque sea en un único grupo implícito— para calcular algo, y eso recién ocurre en el paso 3, con GROUP BY. Pedirle a WHERE que filtre por SUM(monto) es pedirle que use un valor que todavía no se calculó, y la mayoría de los motores lo rechazan con un error. Si la condición depende de un agregado, va en HAVING, que se evalúa en el paso 4. Si no depende de ningún agregado —pedidos confirmados, productos con poco stock— corresponde a WHERE, porque descarta filas antes de que el motor tenga que agruparlas.
¿HAVING puede usarse sin GROUP BY? ¿Qué significa?
Sí, y es un uso poco conocido. Cuando una consulta con agregaciones no tiene GROUP BY, el motor trata la tabla completa —o lo que quedó después de WHERE— como un único grupo implícito, y HAVING filtra ese grupo según el resultado de la agregación. Por ejemplo, SUM(monto) con HAVING SUM(monto) > 1000000 devuelve una fila con ese total si supera el millón, o cero filas si no. Es preguntar «¿este único resultado cumple la condición?», sin una subconsulta. GROUP BY no es lo que hace posible la agregación, sino lo que divide las filas en grupos: sin él, sigue habiendo uno solo —todas las filas juntas— y HAVING lo filtra igual que a cualquier otro.
¿Cuál conviene por rendimiento?
Conviene resolver en WHERE toda condición sobre columnas de las tablas, sin depender de un valor agregado, y dejar HAVING solo para lo que dependa de un COUNT, un SUM o un AVG. La razón es el orden lógico: WHERE actúa en el paso 2 y puede aprovechar un índice para descartar filas sin leerlas todas; cuantas menos sobrevivan, menos trabajo hace GROUP BY. HAVING actúa en el paso 4, sobre un resultado que recién terminó de calcularse y que no es una columna indexada. Mover al WHERE una condición que hoy está en HAVING sin necesidad suele reducir el trabajo del motor, porque descarta filas antes de agrupar en vez de agrupar de más y descartar después.
¿Cuál es el orden real en que un motor evalúa una consulta?
El orden lógico —coincidente, con matices menores, entre PostgreSQL, SQL Server, MySQL y SQLite— es: FROM y los JOIN arman el conjunto de filas; WHERE filtra esas filas una por una; GROUP BY las agrupa según un valor compartido; HAVING filtra los grupos ya formados; recién en quinto lugar SELECT calcula las columnas y asigna los alias; después DISTINCT, si la consulta lo pide; luego ORDER BY, que sí puede usar los alias del SELECT; y finalmente LIMIT (o TOP, o FETCH FIRST según el motor). De este orden salen casi todas las reglas prácticas: por qué un alias del SELECT no sirve en el WHERE, por qué WHERE no acepta agregaciones y HAVING sí, y por qué HAVING puede funcionar sin GROUP BY. Es el modelo mental correcto para razonar sobre cualquier consulta, aunque no siempre coincide con los pasos exactos que el motor ejecuta físicamente por dentro.
¿Por qué en MySQL puedo poner un alias del SELECT dentro de HAVING y en otros motores no?
Porque MySQL documenta esa posibilidad como una extensión propia de GROUP BY y HAVING, que permite referenciar en HAVING un alias del SELECT. En el orden lógico, SELECT se evalúa en el paso 5 y HAVING en el 4, así que en teoría el alias todavía no debería existir —el mismo problema que impide usarlo en un WHERE—. MySQL lo resuelve internamente, pero esa facilidad no forma parte del estándar. Para portabilidad, la forma segura es repetir la expresión completa —HAVING COUNT(*) > 3 en vez de HAVING productos_criticos > 3—. Explica por qué una consulta que funciona en MySQL puede fallar al migrarla a PostgreSQL o a SQL Server.
¿El orden lógico documentado es el orden en que realmente se ejecuta?
No necesariamente. El orden lógico —FROM/JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY y LIMIT— es un modelo conceptual: describe qué información existe disponible en cada punto, y por eso explica qué es válido escribir y qué no. Pero la documentación de SQLite aclara que ese orden de procesamiento es «puramente ilustrativo», y que el optimizador real puede reordenar las operaciones si el resultado final es equivalente. Esto significa que el motor puede aplicar una condición de WHERE antes de terminar de resolver un JOIN, o acceder a los datos en un orden distinto, si eso le permite llegar al mismo resultado con menos trabajo. La forma correcta de saber qué hace un motor con una consulta concreta es revisar su plan de ejecución —con EXPLAIN o su equivalente—, no asumirlo del modelo lógico. Uno sirve para razonar sobre la sintaxis; el otro, sobre el rendimiento real.
Fuentes
Documentación oficial y material de la comunidad consultados para esta guía. Fecha de consulta: 29 de julio de 2026.
Aportes de la comunidad Underc0de
- Underc0de, foro. Sección Bases de datos. No se encontró ningún hilo específico que compare WHERE y HAVING; se enlaza la sección general como punto de partida para consultas de la comunidad.
Documentación oficial
- PostgreSQL. Table Expressions. Orden de evaluación de FROM, WHERE, GROUP BY y HAVING.
- Microsoft. SELECT (Transact-SQL). El orden lógico de procesamiento numerado, y la razón por la que un alias del SELECT no puede usarse en cláusulas anteriores.
- MySQL. GROUP BY y HAVING handling. La extensión que permite referenciar en HAVING un alias del SELECT.
- SQLite. SELECT — Simple Select Processing. La aclaración de que el orden de procesamiento descrito es «puramente ilustrativo» y que el optimizador puede reordenar operaciones.
- PostgreSQL. SQL Conformance. Contexto sobre qué tan estandarizado está el comportamiento descrito entre motores.