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

Ordenar y limitar

1. Qué es

Una tabla no tiene orden. Es un conjunto de filas, y el motor las devuelve como le conviene. Si quieres un orden, tienes que pedirlo.

ORDER BY es el paso 7 del orden-logico-de-ejecucion y LIMIT el 8. De ahí salen las dos ideas del tema:

  • ORDER BY ve el resultado terminado, así que acepta alias y expresiones.
  • LIMIT sin ORDER BY es una pregunta mal hecha, porque no hay nada que garantice qué filas te toca.

2. Sintaxis

SELECT ...
FROM ...
ORDER BY columna [ASC | DESC] [NULLS FIRST | NULLS LAST], columna2 ...
LIMIT n
OFFSET m;
  • ASC es el valor por defecto y casi nadie lo escribe.
  • DESC se aplica a una sola columna, no a las que vienen detrás. ORDER BY a, b DESC ordena a ascendente y b descendente.
  • Se puede ordenar por columna, alias, expresión o número de posición.
  • OFFSET m salta las primeras m filas.
  • La forma del estándar SQL es OFFSET m ROWS FETCH FIRST n ROWS ONLY. PostgreSQL acepta ambas; LIMIT es más corta y es la que se usa.

3. Ejemplos sobre Northwind

Lo básico — los cinco productos más caros:

SELECT product_name, unit_price
FROM products
ORDER BY unit_price DESC
LIMIT 5;
      product_name       | unit_price
-------------------------+------------
 Côte de Blaye           |     263.50
 Thüringer Rostbratwurst |     123.79
 Mishi Kobe Niku         |      97.00
 Sir Rodney's Marmalade  |      81.00
 Carnarvon Tigers        |      62.50

Dos criterios, y el DESC que solo afecta a uno:

SELECT country, city, company_name
FROM customers
ORDER BY country, city DESC
LIMIT 6;
  country  |     city     |        company_name
-----------+--------------+----------------------------
 Argentina | Buenos Aires | Océano Atlántico Ltda.
 Argentina | Buenos Aires | Rancho grande
 Argentina | Buenos Aires | Cactus Comidas para llevar
 Austria   | Salzburg     | Piccolo und mehr
 Austria   | Graz         | Ernst Handel
 Belgium   | Charleroi    | Suprêmes délices

País ascendente (Argentina antes que Austria), ciudad descendente dentro de cada país (Salzburg antes que Graz). Para que las dos fueran descendentes habría que escribir ORDER BY country DESC, city DESC.

Y fíjate en las tres empresas de Buenos Aires: su orden entre sí no está definido. Empatan en las dos columnas del ORDER BY, así que el motor las coloca como quiere.

Paginación con OFFSET

SELECT product_name, unit_price FROM products ORDER BY unit_price DESC LIMIT 5;            -- página 1
SELECT product_name, unit_price FROM products ORDER BY unit_price DESC OFFSET 5 LIMIT 5;   -- página 2
     product_name      | unit_price
-----------------------+------------
 Raclette Courdavault  |      55.00
 Manjimup Dried Apples |      53.00
 Tarte au sucre        |      49.30
 Ipoh Coffee           |      46.00
 Rössle Sauerkraut     |      45.60

Página n = OFFSET (n-1) * tamaño LIMIT tamaño. Funciona, y tiene dos problemas que conviene saber desde ya:

  1. OFFSET no salta gratis. Para darte las filas 100.001 a 100.010, el motor produce y descarta las 100.000 anteriores. En tablas grandes la paginación se vuelve más lenta cuanto más avanzas.
  2. Si el orden no es determinista, las páginas se solapan o pierden filas. Ver abajo.

El empate: por qué LIMIT necesita desempate

Cuatro productos cuestan exactamente 18.00:

SELECT product_name, unit_price FROM products WHERE unit_price = 18.00;
   product_name   | unit_price
------------------+------------
 Chai             |      18.00
 Steeleye Stout   |      18.00
 Chartreuse verte |      18.00
 Lakkalikööri     |      18.00

Si pides "los 20 productos más caros" y el corte cae en mitad de ese empate, cuál de los cuatro entra no está definido. Puede cambiar al reejecutar, al insertar filas, o al actualizar el motor. Nadie te avisará: cada ejecución devuelve 20 filas y todas parecen bien.

La solución es siempre la misma: añade una columna de desempate única.

SELECT product_name, unit_price
FROM products
ORDER BY unit_price DESC, product_id;

product_id es la clave primaria, así que el orden pasa a ser único y reproducible. Regla práctica: si un ORDER BY va a alimentar una paginación, un informe que se compara entre semanas, o un LIMIT, termínalo con la clave primaria.

Los NULL al ordenar

SELECT company_name, region FROM customers ORDER BY region DESC LIMIT 4;
            company_name            | region
------------------------------------+--------
 Ana Trujillo Emparedados y helados |
 Antonio Moreno Taquería            |
 Around the Horn                    |

En PostgreSQL el valor por defecto es NULLS LAST en ASC y NULLS FIRST en DESC — los nulos se tratan como si fueran el valor más grande. Se cambia a mano:

ORDER BY region ASC  NULLS FIRST
ORDER BY region DESC NULLS LAST

Escríbelo siempre que haya nulos y el orden importe. Es el punto donde PostgreSQL y SQL Server dan resultados distintos con la misma consulta (§ 5).

Ordenar texto: el collation

SELECT city FROM customers ORDER BY city LIMIT 8;

Esta consulta no tiene una única respuesta correcta. Depende de dónde la ejecutes:

 en tu PostgreSQL local        en Neon
 (en_US.UTF-8)                 (C.UTF-8)
------------------------      --------------
 Aachen                        Aachen
 Albuquerque                   Albuquerque
 Anchorage                     Anchorage
 Århus                         Barcelona
 Barcelona                     Barquisimeto
 Barquisimeto                  Bergamo
 Bergamo                       Berlin
 Berlin                        Bern

En local, Århus sale cuarto: la Å se ordena como una A. En Neon desaparece del top 8, porque allí el orden es por código de byte y la Å se va detrás de la Z.

No es magia ni un error: es el collation de la base, la regla que decide qué texto va antes que cuál. Y no está en la consulta ni en los datos, sino en la configuración de la base:

SELECT datlocprovider, datcollate, datlocale
FROM pg_database WHERE datname = current_database();

Se puede forzar en la propia consulta:

SELECT city FROM customers ORDER BY city COLLATE "C" LIMIT 8;

Si un informe ordenado alfabéticamente sale distinto en dos servidores, el collation es el primer sospechoso. Es un tema con suficiente enjundia como para tener el suyo propio, desarrollado en el vault: temas/sql/collation-y-orden-del-texto.

Ordenar por algo que no está en el SELECT

Es legal y a veces útil:

SELECT product_name FROM products ORDER BY unit_price DESC LIMIT 3;

Devuelve solo los nombres, ordenados por un precio que no se ve. Cómodo, y peligroso en un informe: quien lo lea no puede verificar el orden. Si el criterio importa, enséñalo.

4. Errores comunes

  1. LIMIT sin ORDER BY. No es "las primeras filas": es "unas filas cualesquiera". Puede cambiar entre ejecuciones.
  2. ORDER BY con empates y sin desempate. El caso de los cuatro productos a 18.00.
  3. Creer que DESC afecta a todas las columnas. Afecta solo a la suya.
  4. Ordenar por número de posición en código que se guarda. ORDER BY 2 es cómodo para probar; el día que alguien añade una columna al SELECT, el orden cambia solo y en silencio.
  5. Esperar que LIMIT 10 haga rápida una consulta lenta. Es el último paso: antes ya se ordenó todo. Ver orden-logico-de-ejecucion § 5.
  6. Paginar con OFFSET sobre un orden no determinista. La fila 10 de la página 1 puede repetirse como fila 1 de la página 2.
  7. Ordenar un número guardado como texto. '10' < '9' es cierto en texto. Si una columna de importes está en varchar, el orden será alfabético y parecerá aleatorio.

5. Diferencias con SQL Server

PostgreSQL SQL Server
Limitar LIMIT 5 TOP 5 (dentro del SELECT)
Saltar y limitar OFFSET 5 LIMIT 5 ORDER BY ... OFFSET 5 ROWS FETCH NEXT 5 ROWS ONLY
Nulos en ASC Al final Al principio
NULLS FIRST/LAST ❌ No existe; se emula con ORDER BY CASE WHEN col IS NULL THEN 1 ELSE 0 END, col
Sensibilidad del orden Collation de la base o de la columna Collation, y por defecto suele ser insensible a mayúsculas

La fila de los nulos es la que rompe migraciones. La misma consulta, los mismos datos, y las filas sin región salen arriba en un motor y abajo en el otro. Si alguien compara los dos informes fila a fila, no cuadran — y la causa no está a la vista en la consulta.

Detalle de entrevista: en SQL Server, TOP sin ORDER BY no garantiza nada, exactamente igual que LIMIT. Es la misma trampa con otro nombre.

6. Ejercicios

  1. Los 10 pedidos con mayor flete, del más caro al más barato, mostrando id, cliente y flete.
  2. Clientes ordenados por país y, dentro de cada país, por ciudad — ambos ascendentes — y con los que no tienen región primero.
  3. Los productos ordenados de más caro a más barato, garantizando que dos ejecuciones dan exactamente el mismo orden.
  4. La segunda página de empleados, con páginas de 3, ordenados por apellido.
  5. Sin ejecutarla, di qué problema tiene: sql SELECT customer_id, order_date FROM orders ORDER BY order_date DESC LIMIT 10;

7. Pregunta de negocio

Dirección quiere "el top 10 de productos más caros" para una presentación, y lo va a repetir cada trimestre para comparar. ¿Qué consulta entregas y qué le adviertes?

SolucionesResuélvelos antes de abrir

1.

SELECT order_id, customer_id, freight
FROM orders
ORDER BY freight DESC
LIMIT 10;
 order_id | customer_id | freight
----------+-------------+----------
    10540 | QUICK       | 1007.64
    10372 | QUEEN       |  890.78
    11030 | SAVEA       |  830.75

El flete máximo es 1007,64, casi trece veces la media de 78,24.

2.

SELECT country, city, region, company_name
FROM customers
ORDER BY country, city, region NULLS FIRST;

Ojo con dónde va el NULLS FIRST: modifica solo a la columna que tiene al lado, no a todo el ORDER BY.

3.

SELECT product_id, product_name, unit_price
FROM products
ORDER BY unit_price DESC, product_id;

La clave está en el product_id final. Sin él, los cuatro productos a 18.00 pueden salir en cualquier orden, y "el mismo orden en dos ejecuciones" deja de estar garantizado.

4.

SELECT employee_id, last_name, first_name
FROM employees
ORDER BY last_name, employee_id
OFFSET 3 LIMIT 3;
 employee_id | last_name | first_name
-------------+-----------+------------
           9 | Dodsworth | Anne
           2 | Fuller    | Andrew
           7 | King      | Robert

Página 2 = saltar 3, tomar 3. La primera se salta a Buchanan, Callahan y Davolio. El employee_id al final está por lo mismo que en el ejercicio 3: paginar sobre un orden no determinista hace que las páginas se solapen.

5. Dos problemas, y el segundo es el grave:

  1. order_date es date, sin hora, y hay varios pedidos por día. Los últimos 10 "por fecha" están llenos de empates: cuáles entran no está definido. Se arregla con ORDER BY order_date DESC, order_id DESC.
  2. No hay WHERE, así que incluye pedidos sin enviar y no distingue nada. Pero sobre todo: la consulta dice "los 10 más recientes" y solo garantiza "10 filas de entre las más recientes". En un informe que alguien va a comparar con el del mes pasado, eso es un fallo, no un detalle.

Pregunta de negocio.

SELECT product_id,
       product_name,
       unit_price
FROM products
WHERE NOT discontinued
ORDER BY unit_price DESC, product_id
LIMIT 10;

Las tres advertencias, por orden de importancia:

  1. Los empates. Aquí el corte cae limpio —el décimo cuesta 40,00 y el undécimo 38,00— pero dentro del top hay un empate: Schoggi Schokolade y Vegie-spread cuestan los dos 43,90 y ocupan los puestos 8 y 9 por product_id, no por precio. El día que el corte caiga sobre un empate así, el product_id hará la consulta reproducible — pero reproducible no es justo: habrá un producto fuera del top por un criterio que no tiene nada que ver con el precio. La respuesta honesta ese día es enseñar 11 filas y decir que hay empate.
  2. "Producto más caro" no es "producto que más factura". unit_price es el precio de catálogo; un producto carísimo que nadie compra encabeza esta lista. Si la pregunta de Dirección es de negocio, la métrica probablemente sea sum(unit_price * quantity) sobre order_details, no unit_price — y eso necesita un JOIN (joins-multiples-y-granularidad).
  3. "Cada trimestre" implica comparar. He filtrado los descatalogados a propósito: si no, un producto puede salir del top no por bajar de precio sino por dejar de venderse, y la comparación entre trimestres mezcla dos causas distintas. Esa decisión hay que contarla, no esconderla en el WHERE.