Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
El orden en que escribe no es el orden en que se ejecuta
La mayoría de los errores confusos de SQL nacen de un solo hecho: el orden de las cláusulas que usted escribe no es el orden en que la base de datos las evalúa. SELECT se escribe primero, pero el motor llega a él tarde. Por eso esto falla:
SELECT price * quantity AS total
FROM order_items
WHERE total > 1000; -- ERROR: no existe la columna 'total'
y esto funciona:
SELECT price * quantity AS total
FROM order_items
ORDER BY total DESC; -- correcto
El alias no ha cambiado. Lo que cambia es cuándo se ejecuta cada cláusula respecto a SELECT.
Los dos órdenes, lado a lado
| Orden escrito | Orden lógico de ejecución |
|---|---|
1. SELECT | 1. FROM / JOIN — reunir las filas |
2. FROM | 2. WHERE — filtrar filas individuales |
3. JOIN | 3. GROUP BY — agrupar las filas |
4. WHERE | 4. HAVING — filtrar los grupos |
5. GROUP BY | 5. SELECT — calcular las columnas de salida (aquí nacen los alias) |
6. HAVING | 6. DISTINCT — eliminar filas duplicadas |
7. ORDER BY | 7. ORDER BY — ordenar el resultado |
8. LIMIT | 8. LIMIT / OFFSET — recortar el resultado |
Lea la columna derecha de arriba abajo y la mayoría de las sorpresas de SQL dejan de serlo. Este es el orden lógico que define el estándar; el optimizador puede reordenar el trabajo real mientras el resultado coincida.
Por qué los alias fallan en WHERE y funcionan en ORDER BY
SELECT es el paso 5 y WHERE el paso 2. Cuando se ejecuta WHERE, el alias todavía no existe. ORDER BY es el paso 7, posterior a SELECT, así que para entonces sí existe.
La solución portable es repetir la expresión:
SELECT price * quantity AS total
FROM order_items
WHERE price * quantity > 1000;
O empujarla a una subconsulta, con lo que el alias pasa a ser una columna real para la consulta externa:
SELECT * FROM (
SELECT price * quantity AS total FROM order_items
) t
WHERE t.total > 1000;
Los dialectos flexibilizan esta regla de formas distintas: MySQL admite alias en GROUP BY y HAVING; PostgreSQL los admite en GROUP BY y ORDER BY, pero no en HAVING. Ninguna base de datos relevante admite un alias en WHERE.
WHERE frente a HAVING
No son intercambiables, y la razón vuelve a ser el orden: WHERE (paso 2) filtra filas antes de agrupar; HAVING (paso 4) filtra grupos después de agregar.
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE status = 'paid' -- primero las filas: solo pedidos pagados
GROUP BY customer_id
HAVING COUNT(*) >= 3; -- después los grupos: 3 o más
Intercambiarlas cambia el significado por completo. HAVING status = 'paid' pregunta por un grupo, no por una fila, y WHERE COUNT(*) >= 3 es un error directamente: en el paso 2 aún no se ha contado nada.
Regla práctica: si la condición menciona una función de agregación, va en HAVING; en caso contrario, en WHERE. Además, dejar las condiciones no agregadas en WHERE permite descartar filas antes, lo que suele ser más rápido.
GROUP BY: la regla que cambia según la base de datos
Al agrupar, cada columna seleccionada debe estar en la lista de GROUP BY o ir envuelta en una función de agregación. De lo contrario, la base de datos no puede saber cuál de las filas agrupadas mostrar.
-- Roto: ¿qué name acompaña al recuento?
SELECT customer_id, name, COUNT(*) FROM orders GROUP BY customer_id;
PostgreSQL lo rechaza sin más. MySQL también lo rechaza por defecto: desde la versión 5.7.5 el modo ONLY_FULL_GROUP_BY viene activado. Las configuraciones antiguas lo aceptaban y devolvían el valor de una fila arbitraria, y por eso consultas heredadas se rompen justo al actualizar el servidor con el mensaje “expression is not in GROUP BY clause and contains nonaggregated column”. La solución es añadir la columna a GROUP BY o agregarla (MAX(name)), no desactivar el modo.
Agregaciones y NULL
Dos comportamientos que conviene memorizar porque alteran los números en silencio:
| Expresión | ¿Cuenta los NULL? |
|---|---|
COUNT(*) | Sí — cuenta filas |
COUNT(columna) | No — omite las filas con NULL en esa columna |
COUNT(DISTINCT columna) | No — solo valores distintos no nulos |
SUM / AVG / MAX / MIN | No — ignoran los NULL |
AVG(columna) divide entre el número de valores no nulos, no entre el número de filas. Si NULL debe contar como cero, dígalo explícitamente con AVG(COALESCE(columna, 0)).
Donde más importa es después de un LEFT JOIN: las filas sin coincidencia producen NULL en el lado derecho, así que COUNT(*) cuenta como 1 a un cliente sin pedidos, mientras que COUNT(o.id) cuenta correctamente 0.
SELECT c.id, COUNT(o.id) AS order_count -- no COUNT(*)
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id;
LIMIT se ejecuta al final y necesita un desempate
LIMIT es el paso 8, posterior a la ordenación. De ahí, dos consecuencias.
No abarata la agregación. Un LIMIT 10 sobre una consulta con GROUP BY sigue agrupándolo todo y después descarta casi todo.
Sin una clave de orden única, la paginación es inestable. Si created_at tiene empates, una fila puede aparecer en dos páginas o en ninguna. Añada un desempate único:
ORDER BY created_at DESC, id DESC
LIMIT 20 OFFSET 40;
La sintaxis varía: PostgreSQL y MySQL aceptan LIMIT n OFFSET m (MySQL conserva además el antiguo LIMIT m, n, con los argumentos invertidos), mientras que SQL Server usa OFFSET m ROWS FETCH NEXT n ROWS ONLY.
La trampa de JOIN con WHERE
JOIN es el paso 1 y WHERE el paso 2, así que una condición sobre la tabla derecha se aplica después de que el join externo haya insertado los NULL, y esas filas no superan la condición:
-- Se comporta como un INNER JOIN sin avisar
SELECT * FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid';
-- Conserva a los clientes sin pedidos pagados
SELECT * FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid';
Las condiciones que filtran el join van en ON; las que filtran el resultado final van en WHERE. El desglose completo de tipos de join está en la referencia de JOIN en SQL.
Construir consultas sin memorizar el orden
Conocer el orden es una cosa; colocar cada cláusula en su sitio siempre es otra. El Visual SQL Builder permite elegir tablas, condiciones de join, filtros, agrupación y ordenación desde un formulario y emite las cláusulas en el orden escrito correcto. Como el SQL generado cambia a medida que añade piezas, también sirve para aprender dónde va realmente cada condición.
Para leer una consulta existente, el Formateador SQL la indenta hasta hacer evidentes los límites entre cláusulas, y SQL a Diagrama ER muestra cómo se relacionan las tablas que está uniendo. Si todavía está diseñando las tablas, la referencia de CREATE TABLE recoge tipos y restricciones. Todo funciona dentro del navegador: las consultas y esquemas que pega no se envían a ningún sitio.
Resumen
- Orden lógico:
FROM→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT - Los alias nacen en
SELECT:WHEREno los ve,ORDER BYsí - Condiciones sin agregación en
WHERE; con agregación enHAVING ONLY_FULL_GROUP_BYes el valor por defecto de MySQL desde 5.7.5COUNT(*)cuenta filas;COUNT(columna)omite NULL. Tras unLEFT JOINcasi siempre quiere lo segundoLIMITva al final y necesita un desempate único enORDER BY- Filtre el join en
ONy el resultado enWHERE
Preguntas frecuentes
¿Por qué mi alias funciona en ORDER BY pero no en WHERE?
Porque WHERE se evalúa antes que SELECT y ORDER BY después. Los alias los crea SELECT, así que simplemente no existen cuando se ejecuta WHERE. Repita la expresión completa en WHERE o envuelva la consulta en una subconsulta para que el alias pase a ser una columna real.
¿Cuál es la diferencia entre WHERE y HAVING?
WHERE filtra filas individuales antes de agrupar; HAVING filtra grupos después de agregar. Si su condición usa una función de agregación como COUNT() o SUM(), tiene que ir en HAVING. El resto debería quedarse en WHERE, donde puede eliminar filas antes.
¿Por qué MySQL me da “expression is not in GROUP BY clause”?
Es el modo SQL ONLY_FULL_GROUP_BY, activado por defecto desde MySQL 5.7.5. Cada columna seleccionada debe aparecer en GROUP BY o ir dentro de una agregación. Es la causa habitual de que consultas antiguas se rompan al actualizar: añada la columna a GROUP BY o agréguela en lugar de desactivar el modo.
¿Añadir LIMIT acelera una consulta con agregación?
Por lo general no. LIMIT se aplica al final, después de agrupar y ordenar, así que la base de datos construye igualmente el resultado completo antes de descartar filas. Para abaratarla, reduzca el trabajo antes con condiciones WHERE o con un índice que apoye la agrupación.
¿El SQL que pego se envía a un servidor?
No. El Visual SQL Builder y el Formateador SQL funcionan íntegramente en su navegador: las consultas y los nombres de tabla que introduce no se transmiten a ningún sitio.