Curso SQL · Northwind
Tema 5 de 60
Bloque 2 · La consulta PostgreSQL 17 northwind

El orden lógico de ejecución

1. Qué es

Una consulta se escribe en un orden y se ejecuta en otro. Ese desajuste explica la mitad de los errores de SQL, y una vez que lo tienes claro dejas de memorizar reglas sueltas.

Se escribe así:

SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT

Se ejecuta así:

FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT

SELECT —lo primero que escribes— es lo penúltimo que ocurre. Todo lo demás se deduce de ahí.

Este orden es lógico, no físico. Es el orden en que el motor está obligado a comportarse como si hiciera las cosas. Por dentro puede reordenarlas todo lo que quiera mientras el resultado no cambie (§ 5 lo demuestra).

2. Los siete pasos, uno por uno

# Paso Qué hace Qué existe ya
1 FROM Trae las filas de la tabla (o las une, si hay JOIN) Columnas de la tabla
2 WHERE Descarta filas, una por una Columnas de la tabla
3 GROUP BY Colapsa las filas supervivientes en grupos Columnas de la tabla
4 HAVING Descarta grupos Columnas agrupadas + agregados
5 SELECT Calcula las columnas de salida y crea los alias Todo lo anterior
6 DISTINCT Elimina filas de salida repetidas Columnas de salida
7 ORDER BY Ordena el resultado Alias incluidos
8 LIMIT Corta El resultado ya ordenado

De esa tabla salen, sin memorizar nada, las cuatro reglas que todo el mundo aprende a golpes:

  • WHERE no puede usar un alias del SELECT → el alias todavía no existe (paso 5 > paso 2).
  • WHERE no puede usar funciones de agregación → no hay grupos todavía (paso 3 > paso 2).
  • ORDER BY sí puede usar un alias → se ejecuta después del SELECT.
  • HAVING filtra grupos; WHERE filtra filas.

3. Ejemplos sobre Northwind

Una consulta con todos los pasos. 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
LIMIT 5;
  pais   | pedidos
---------+---------
 USA     |      61
 Germany |      58
 Austria |      33
 Brazil  |      32
 France  |      27

Ahora el embudo, medido de verdad, paso a paso:

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-7. SELECT, ORDER BY, LIMIT 5 5 filas

Cada número de esa tabla se puede comprobar por separado:

SELECT count(*) FROM orders;                      -- 830
SELECT count(*) FROM orders WHERE freight > 50;   -- 360

El error clásico 1 — alias en el WHERE:

SELECT ship_country AS pais
FROM orders
WHERE pais = 'Peru';
ERROR:  column "pais" does not exist

Cuando el WHERE se evalúa, pais no se ha inventado todavía. La solución es repetir la expresión: WHERE ship_country = 'Peru'.

El error clásico 2 — agregado en el WHERE:

SELECT ship_country, count(*)
FROM orders
WHERE count(*) > 10
GROUP BY ship_country;
ERROR:  aggregate functions are not allowed in WHERE

Aquí PostgreSQL es explícito, y con razón: en el paso 2 cada fila está sola, no pertenece a ningún grupo, así que no hay nada que contar. Eso es exactamente para lo que existe HAVING.

La asimetría que sorprende a todo el mundo. GROUP BY acepta el alias:

SELECT ship_country AS pais, count(*) AS pedidos
FROM orders
GROUP BY pais            -- ✅ funciona
HAVING pedidos > 10;     -- ❌ ERROR: column "pedidos" does not exist

Si el orden lógico manda, GROUP BY (paso 3) tampoco debería ver el alias. Y en teoría no lo ve: es una comodidad que el estándar SQL concede solo a GROUP BY y a ORDER BY, no a WHERE ni a HAVING. La regla práctica es la que importa:

En GROUP BY y ORDER BY puedes usar el alias. En WHERE y HAVING, nunca.

ORDER BY también acepta el número de posición:

SELECT product_name, unit_price
FROM products
ORDER BY 2 DESC
LIMIT 3;
      product_name       | unit_price
-------------------------+------------
 Côte de Blaye           |     263.50
 Thüringer Rostbratwurst |     123.79
 Mishi Kobe Niku         |      97.00

2 es la segunda columna del SELECT. Funciona porque ORDER BY ve el resultado ya construido. Cómodo para probar, frágil para guardar: el día que alguien añade una columna al SELECT, el orden cambia solo.

4. Errores comunes

  1. Poner en HAVING lo que va en WHERE. Filtrar por una columna de la tabla en HAVING funciona, pero es confuso de leer y solo es correcto si esa columna está en el GROUP BY. Si no, error. Filtra filas en WHERE, grupos en HAVING.
  2. Esperar que LIMIT acelere una consulta lenta. LIMIT es el paso 8. Antes de cortar, el motor ya escaneó, filtró, agrupó y ordenó todo. LIMIT 10 sobre un ORDER BY de un millón de filas no evita ordenar el millón.
  3. Creer que WHERE se evalúa de izquierda a derecha y protege al resto. No hay garantía de orden dentro del WHERE. WHERE x <> 0 AND 100/x > 5 puede dar división por cero. Para eso está CASE (case-condicionales-en-sql).
  4. Confundir este orden con el rendimiento. Es un orden lógico, no una instrucción de ejecución. Ver abajo.

5. Orden lógico ≠ orden real

Se demuestra en 20 segundos. Estas dos consultas dan lo mismo (83 pedidos de Brasil), pero la segunda "debería" agrupar los 21 países y descartar 20 después:

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';

Pidiéndole al motor su plan con EXPLAIN, salen idénticos:

 GroupAggregate  (cost=0.00..25.59 rows=1 width=14)
   ->  Seq Scan on orders  (cost=0.00..25.38 rows=83 width=6)
         Filter: ((ship_country)::text = 'Brazil'::text)

El planificador empujó el filtro hacia abajo y filtró antes de agrupar, aunque escribieras HAVING. Puede hacerlo porque el resultado es demostrablemente el mismo.

Esto no es permiso para escribir mal. El motor optimiza los casos evidentes; los no evidentes se quedan como los escribiste. Y quien lea tu consulta no tiene un planificador dentro. Ver explain-y-explain-analyze.

6. Diferencias con SQL Server

El orden lógico es idéntico — es del estándar SQL, no del motor. Cambia solo el último paso: SQL Server usa TOP n (que va dentro del SELECT) u OFFSET ... FETCH, y PostgreSQL usa LIMIT.

Detalle de entrevista: en SQL Server, TOP sin ORDER BY no garantiza qué filas devuelve. En PostgreSQL, LIMIT sin ORDER BY tampoco. Es la misma trampa con distinto nombre.

7. Ejercicios

  1. Arregla esta consulta sin cambiar lo que devuelve: sql SELECT unit_price * units_in_stock AS valor FROM products WHERE valor > 1000;
  2. Arregla esta: sql SELECT customer_id, count(*) FROM orders WHERE count(*) > 20 GROUP BY customer_id;
  3. Sin ejecutarla, di cuántas filas devuelve y por qué: sql SELECT ship_city FROM orders WHERE ship_country = 'Brazil' GROUP BY ship_city LIMIT 3;
  4. Escribe la consulta que responde: empleados que gestionaron más de 100 pedidos, del que más al que menos. Usa las cinco cláusulas.
  5. Explica en una línea por qué esto no da error, aunque precio_final sea un alias: sql SELECT product_name, unit_price * 1.18 AS precio_final FROM products ORDER BY precio_final DESC;

8. Pregunta de negocio

Un compañero te enseña una consulta que tarda 40 segundos y te dice: "le puse LIMIT 10, no entiendo por qué sigue tardando lo mismo". ¿Qué le respondes, y qué le propones mirar primero?

SolucionesResuélvelos antes de abrir

1. Repetir la expresión en el WHERE. El alias no existe en el paso 2:

SELECT unit_price * units_in_stock AS valor
FROM products
WHERE unit_price * units_in_stock > 1000;

Devuelve 25 productos. (La alternativa elegante —calcularlo una sola vez— es una tabla derivada o un CTE: cte-with.)

2. El filtro es sobre grupos, así que va en HAVING:

SELECT customer_id, count(*)
FROM orders
GROUP BY customer_id
HAVING count(*) > 20;
 customer_id | count
-------------+-------
 ERNSH       |    30
 QUICK       |    28
 SAVEA       |    31

3. 3 filas, pero la pregunta real es cuáles. El embudo: WHERE deja 83 pedidos de Brasil, GROUP BY ship_city los colapsa en 4 ciudades (Brasil entero se surte de cuatro), y LIMIT 3 corta tres. Sin ORDER BY, cuáles tres no está definido — el motor devuelve las que le resulten cómodas, y puede cambiar de opinión mañana. LIMIT sin ORDER BY es una pregunta mal hecha.

4.

SELECT employee_id AS empleado,
       count(*)    AS pedidos
FROM orders
GROUP BY empleado
HAVING count(*) > 100
ORDER BY pedidos DESC;
 empleado | pedidos
----------+---------
        4 |     156
        3 |     127
        1 |     123
        8 |     104

Cuatro de los nueve empleados. Que salga el id y no el nombre es una limitación real de este punto del curso: para eso hace falta un JOIN (inner-join).

5. Porque ORDER BY es el paso 7 y SELECT el 5: cuando se ordena, el alias ya existe.

Pregunta de negocio.

Qué le respondes: LIMIT es el último paso. El motor primero escanea, filtra, agrupa y ordena el conjunto completo; solo cuando ya tiene el resultado final ordenado, se queda con 10 filas. LIMIT reduce lo que se te envía, no lo que se trabaja.

Qué le propones mirar, en este orden:

  1. EXPLAIN ANALYZE — dónde se va el tiempo de verdad (explain-y-explain-analyze).
  2. El WHERE — si el filtro puede ser más selectivo, ahí se gana de verdad, porque es el paso 2 y reduce todo lo que viene detrás.
  3. El ORDER BY — ordenar millones de filas para enseñar diez es lo más caro de la consulta. Un índice sobre la columna de orden puede evitar el Sort entero (indices).

La frase que resume el tema: para que una consulta sea rápida hay que atacar el principio del embudo, no el final.