Curso SQL · Northwind
Tema 3 de 60
Bloque 1 · Antes de escribir SQL PostgreSQL 17 northwind

Leer un esquema desconocido

1. Qué es

Te sientan frente a una base de datos que no conoces. Cuarenta tablas, nombres en inglés, nadie que te explique nada. Esto pasa el primer día de casi cualquier trabajo con datos.

Este tema es el método para orientarte sin depender de que alguien te lo cuente.

2. Las cuatro preguntas

Ante cualquier tabla nueva, en este orden:

# Pregunta Qué te dice
1 ¿Cuál es el grano? Qué representa una fila
2 ¿Cuál es la clave primaria? Qué identifica esa fila
3 ¿A qué apunta y quién le apunta? Cómo se conecta con el resto
4 ¿Qué pregunta de negocio responde? Para qué existe

2.1 El grano es la más importante

Grano = qué representa una fila. Y se dice en una frase con sujeto:

  • orders"un pedido"
  • order_details"un producto dentro de un pedido"
  • customers"un cliente"

Si no puedes decirlo en una frase, todavía no entendiste la tabla.

Por qué importa tanto: el grano determina qué puedes sumar sin equivocarte. orders.freight es el flete del pedido. Si unes orders con order_details y sumas freight, lo estás sumando una vez por cada línea del pedido. El pedido 11077 tiene 25 líneas: su flete contaría 25 veces.

Ese error no da ningún mensaje. Solo da un número más grande.

3. Cómo obtener las respuestas

3.1 Con DBeaver

Doble clic en la tabla y ahí están las pestañas: Columns, Constraints, Foreign Keys, References. La pestaña ER Diagram dibuja el esquema solo.

3.2 Con psql

\dt              lista las tablas
\d orders        describe una tabla: columnas, tipos, PK, FK y quién le apunta
\d+ orders       lo mismo, más tamaño y comentarios

\d orders es el comando que más vas a usar en tu vida con PostgreSQL.

3.3 Con SQL puro

Cuando el cliente no ayuda, information_schema siempre está. Es SQL estándar: funciona en PostgreSQL, SQL Server, MySQL y casi cualquier motor.

Todas las conexiones del esquema de un vistazo:

SELECT tc.table_name AS tabla,
       kcu.column_name AS columna,
       ccu.table_name AS apunta_a
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
  ON kcu.constraint_name = tc.constraint_name
JOIN information_schema.constraint_column_usage ccu
  ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
  AND tc.table_schema = 'public'
ORDER BY 1, 2;
        tabla         |   columna    |  apunta_a
----------------------+--------------+-------------
 employee_territories | employee_id  | employees
 employee_territories | territory_id | territories
 employees            | reports_to   | employees      ← apunta a sí misma
 order_details        | order_id     | orders
 order_details        | product_id   | products
 orders               | customer_id  | customers
 orders               | employee_id  | employees
 orders               | ship_via     | shippers
 products             | category_id  | categories
 products             | supplier_id  | suppliers
 territories          | region_id    | region

Once claves foráneas y ahí está el mapa completo. En treinta segundos, sin que nadie te explique nada.

Y mira la tercera fila: employees.reports_to apunta a employees. Una tabla que se apunta a sí misma es siempre una jerarquía — aquí, quién le reporta a quién.

4. Ejemplos sobre Northwind

Comprobar el grano. Si la clave primaria es order_id, cada pedido debe aparecer una sola vez:

SELECT count(*) AS filas, count(DISTINCT order_id) AS pedidos_distintos
FROM orders;
 filas | pedidos_distintos
-------+-------------------
   830 |               830

Iguales: el grano es "un pedido". Si no fueran iguales, tu idea del grano estaría mal — y toda suma que hagas después, también.

Ver qué puede faltar. Las columnas que admiten NULL te dicen dónde el negocio permite ausencias:

SELECT column_name, is_nullable
FROM information_schema.columns
WHERE table_name = 'orders' AND is_nullable = 'YES'
ORDER BY ordinal_position;

shipped_date admite NULL. Eso no es un descuido: significa "pedido aún no despachado". El esquema te está contando una regla de negocio.

5. Errores comunes

  1. Empezar por SELECT * en la tabla más grande. Ver 2.155 filas no te dice nada del modelo. Empieza por las claves foráneas.
  2. Asumir el grano por el nombre. Una tabla llamada ventas puede tener una fila por venta, por línea de venta o por venta y día. Compruébalo con el count(*) contra count(DISTINCT).
  3. Ignorar los NULL. Cada columna nullable es una regla de negocio escrita en el esquema.
  4. No mirar si una tabla se apunta a sí misma. Es la señal de una jerarquía, y cambia por completo cómo se consulta.

6. Ejercicios

  1. Describe el grano de las 12 tablas de Northwind, en una frase cada una.
  2. Comprueba con SQL que el grano de order_details es "un producto dentro de un pedido".
  3. ¿Cuántas tablas apuntan a employees? ¿Qué significa cada una?
  4. us_states no tiene ninguna clave foránea. ¿Se puede unir de todas formas con alguna tabla? ¿Con cuál y por qué columna?

7. Pregunta de negocio

Te acaban de dar acceso a Northwind y tu jefe pregunta: "¿por qué canal llegan los pedidos más caros?". Antes de escribir una sola consulta, ¿qué tablas necesitas y cómo se conectan?

Responde con el camino de tablas, no con SQL.

SolucionesResuélvelos antes de abrir

1.

Tabla Grano
region una región comercial
territories un territorio de venta
categories una categoría de producto
suppliers un proveedor
shippers un transportista
employees un empleado
employee_territories la asignación de un empleado a un territorio
customers un cliente
products un producto
orders un pedido
order_details un producto dentro de un pedido
us_states un estado de EE.UU.

2.

SELECT count(*) AS filas,
       count(DISTINCT (order_id, product_id)) AS combinaciones
FROM order_details;

Ambos dan 2.155. Si en cambio cuentas DISTINCT order_id salen 830: eso confirma que el pedido no identifica la fila por sí solo. Hacen falta las dos columnas.

3. Dos: orders.employee_id (qué empleado tomó el pedido) y employee_territories.employee_id (qué territorios cubre). Más una tercera especial: employees.reports_to, que apunta a la propia tabla — la jerarquía de jefes.

4. Sí. customers.region guarda abreviaturas de estado para los clientes de EE.UU., así que se puede unir con us_states.state_abbr:

SELECT count(*) FROM customers c
JOIN us_states s ON c.region = s.state_abbr;

Devuelve 13. Es una unión sin clave foránea declarada: funciona porque los valores coinciden, pero nada garantiza que sigan coincidiendo. El motor no la protege.

Pregunta de negocio: el camino es

shippers ──< orders ──< order_details >── products

shippers da el canal (company_name), orders conecta el pedido con su transportista (ship_via), y el importe no está en orders — hay que calcularlo desde order_details como unit_price * quantity * (1 - discount).

El punto del ejercicio es notar que orders no guarda el total del pedido. Es el error más común al llegar a Northwind: buscar una columna "total" que no existe, porque el importe vive a otro grano.