system onlinepath: /guias/bases-de-datos/where-vs-having-en-sql/mode: knowledge_baselocal:
Datos y bases de datos · Nivel inicial

WHERE vs. HAVING en SQL: diferencias con ejemplos

Por qué WHERE filtra filas antes de agrupar y HAVING filtra grupos después, qué pasa cuando falta el GROUP BY, qué relación tiene todo esto con los índices, y una extensión de MySQL que conviene conocer antes de que te sorprenda.

14 min de lectura▣ Actualizada el ◇ Por Underc0de
Respuesta rápida

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
  1. 01El orden lógico de evaluación de una consulta
  2. 02WHERE: filtra filas antes de que existan grupos
  3. 03HAVING: filtra grupos después de calcularlos
  4. 04HAVING sin GROUP BY: la tabla entera como un grupo
  5. 05WHERE vs. HAVING: tabla comparativa
  6. 06Por qué WHERE puede usar índices y HAVING no
  7. 07La extensión de MySQL: alias del SELECT en HAVING
  8. 08Un modelo lógico, no el plan de ejecución real
  9. 09Errores frecuentes
  10. 10Preguntas frecuentes
  11. 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:

  1. FROM / JOINArma el conjunto de filas de partida, combinando las tablas que participan.
  2. WHEREFiltra esas filas una por una, antes de que exista ningún grupo.
  3. GROUP BYJunta en un grupo las filas que comparten un mismo valor.
  4. HAVINGFiltra los grupos ya formados, según el resultado de una función de agregación.
  5. SELECTRecién acá se calculan las columnas del resultado y se asignan los alias.
  6. DISTINCTElimina filas duplicadas del resultado que ya calculó el SELECT.
  7. ORDER BYOrdena ese resultado final, y por eso sí puede usar los alias creados en el SELECT.
  8. LIMIT(o TOP, o FETCH FIRST según el motor) recorta la cantidad de filas que se devuelven.
Diagrama de ocho pasos en dos filas que muestra el orden lógico en que un motor SQL evalúa una consulta: FROM y JOIN arman el conjunto de filas; WHERE filtra filas individuales y puede aprovechar índices; GROUP BY junta filas en grupos; HAVING filtra los grupos ya formados según el resultado de una función de agregación y no puede usar índices directamente porque actúa sobre un valor recién calculado; SELECT calcula las columnas y los alias; DISTINCT elimina filas duplicadas; ORDER BY ordena el resultado y sí puede usar los alias del SELECT; y LIMIT recorta la cantidad de filas devueltas. Debajo del diagrama, dos notas explican por qué WHERE puede usar índices y HAVING no, y que este orden es un modelo lógico conceptual, no necesariamente el plan de ejecución físico real.
Ocho pasos fijos deciden qué información existe disponible en cada momento de la consulta. WHERE actúa en el paso 2, sobre filas sueltas; HAVING actúa en el paso 4, sobre grupos ya resueltos.

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:

SQL
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:

SQL
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:

SQL
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:

SQL
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.

SQL
SELECT SUM(monto) AS total_facturado
FROM   pedidos
WHERE  estado = 'confirmado'
HAVING SUM(monto) > 1000000;
i
Idea clave

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.

Comparación entre WHERE y HAVING en SQL
AspectoWHEREHAVING
Cuándo se evalúaPaso 2: después de FROM/JOIN, antes de agruparPaso 4: después de GROUP BY y de calcular las agregaciones
Qué filtraFilas individuales de las tablasGrupos ya formados, según su resultado agregado
Funciones de agregaciónNo puede usarlasEs su terreno natural: COUNT, SUM, AVG, MIN, MAX
Necesita GROUP BYNo tiene relación con GROUP BYNo: sin él, trata toda la tabla como un solo grupo
Uso de índicesPuede aprovechar un índice sobre la columna filtradaNo actúa directamente sobre columnas indexadas
Costo relativoDescarta filas temprano, antes de agruparActúa sobre un resultado ya calculado, después de agrupar todo lo que pasó el WHERE
Alias del SELECTNo, en ningún motor estándarEn 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.

Regla práctica

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:

SQL
-- 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:

SQL
-- 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.

!
Por qué esto importa

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

  1. 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

  1. PostgreSQL. Table Expressions. Orden de evaluación de FROM, WHERE, GROUP BY y HAVING.
  2. 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.
  3. MySQL. GROUP BY y HAVING handling. La extensión que permite referenciar en HAVING un alias del SELECT.
  4. 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.
  5. PostgreSQL. SQL Conformance. Contexto sobre qué tan estandarizado está el comportamiento descrito entre motores.