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

Qué es una base de datos relacional

1. Qué es

Una base de datos relacional guarda información en tablas, y las tablas se conectan entre sí.

Una tabla tiene tres reglas que una hoja de cálculo no cumple:

  1. Filas y columnas, con encabezados.
  2. Cada columna tiene un solo tipo de dato. Si precio es numérico, no puedes escribir "consultar" en una celda.
  3. Cada encabezado es distinto. No hay dos columnas llamadas igual.

2. Por qué no una hoja de cálculo

Imagina que llevas los pedidos de tu negocio en Excel. Una fila por pedido:

pedido cliente teléfono ciudad producto cantidad
1001 Panadería Sol 555-1234 Lima Café 10
1002 Panadería Sol 555-1234 Lima Azúcar 5
1003 Panadería Sol 555-9999 Lima Café 8

Tres problemas, y los tres son el mismo:

  1. El teléfono está repetido tres veces. Si cambia, hay que cambiarlo en todas — y en la fila 1003 ya se te escapó una.
  2. No sabes cuál es el correcto. ¿555-1234 o 555-9999? La hoja no tiene forma de decírtelo.
  3. Si borras los pedidos, pierdes el teléfono. El dato del cliente vivía dentro del pedido.

El modelo relacional resuelve los tres de un golpe: cada cosa se guarda una sola vez, en su propia tabla.

clientes                          pedidos
+----+----------------+------+    +------+----+------------+
| id | nombre         | tel  |    | id   | id_cliente | fecha |
+----+----------------+------+    +------+----+------------+
| 7  | Panadería Sol  | 555… |◄───| 1001 | 7  | 2026-03-01 |
+----+----------------+------+    | 1002 | 7  | 2026-03-02 |
                                   +------+----+------------+

El teléfono existe una vez. Los pedidos no lo guardan: guardan a quién apuntan.

3. Las tres piezas del vocabulario

3.1 Clave primaria (PRIMARY KEY)

La columna que identifica a cada fila sin ambigüedad. No se repite y no puede estar vacía.

En Northwind, customers.customer_id es la clave primaria de los clientes.

3.2 Clave foránea (FOREIGN KEY)

Una columna que apunta a la clave primaria de otra tabla. orders.customer_id apunta a customers.customer_id.

Y aquí está la parte que importa: el motor la hace cumplir. Si intentas registrar un pedido de un cliente que no existe, lo rechaza:

INSERT INTO orders (customer_id, employee_id, order_date)
VALUES ('ZZZZZ', 1, '1997-01-01');
ERROR: insert or update on table "orders" violates foreign key constraint

No es una advertencia. Es una negativa. Tu base no puede quedar en un estado incoherente.

Esto distingue a una base de datos relacional de un almacén analítico como BigQuery, donde las claves foráneas son solo documentación y nadie las comprueba. Ver produccion-vs-analisis-oltp-y-olap.

3.3 Cardinalidad

Cuántas filas de una tabla se corresponden con cuántas de la otra.

Tipo Ejemplo en Northwind
Uno a muchos Un cliente tiene muchos pedidos
Muchos a muchos Un pedido lleva muchos productos, y un producto aparece en muchos pedidos

El "muchos a muchos" no se puede guardar directo — hace falta una tabla en medio. En Northwind es order_details, y por eso existe.

4. Ejemplos sobre Northwind

Cuántos pedidos tiene cada cliente — un "uno a muchos" en acción:

SELECT c.company_name, count(o.order_id) AS pedidos
FROM customers c
LEFT JOIN orders o USING (customer_id)
GROUP BY c.company_name
ORDER BY pedidos DESC
LIMIT 3;
    company_name    | pedidos
--------------------+---------
 Save-a-lot Markets |      31
 Ernst Handel       |      30
 QUICK-Stop         |      28

Y la tabla del medio, order_details, que resuelve el "muchos a muchos":

SELECT order_id, count(*) AS lineas
FROM order_details
GROUP BY order_id
ORDER BY lineas DESC
LIMIT 3;
 order_id | lineas
----------+--------
    11077 |     25
    10657 |      6

El pedido 11077 tiene 25 productos distintos. Sin order_details, esos 25 no cabrían en una fila de orders.

5. Errores comunes

  1. Creer que "relacional" significa "las tablas se relacionan". Significa que los datos se guardan como relaciones (tablas). Que se conecten es consecuencia, no definición.
  2. Meter varios valores en una celda. "Café, Azúcar, Harina" en una columna es exactamente lo que el modelo relacional viene a evitar. Eso son tres filas.
  3. Confundir clave primaria con "la primera columna". Es la que identifica, esté donde esté. En order_details son dos columnas juntas: order_id y product_id.

6. Ejercicios

  1. Sin ejecutar nada: si un cliente cambia de teléfono, ¿cuántas filas hay que modificar en Northwind? ¿Y en la hoja de cálculo del punto 2?
  2. Lista las claves primarias de las 12 tablas. Pista: information_schema.table_constraints unida con information_schema.key_column_usage.
  3. ¿Qué tabla tiene una clave primaria formada por dos columnas? ¿Por qué tiene sentido que sea así?
  4. Encuentra en Northwind otra relación "muchos a muchos" distinta de la de pedidos y productos.

7. Pregunta de negocio

El gerente quiere saber si hay clientes registrados que nunca hayan comprado nada, para llamarlos.

No te digo qué columnas ni qué tablas. Piensa primero qué significa "nunca compró" en términos de las dos tablas.

SolucionesResuélvelos antes de abrir

1. Una sola fila, en customers. Los pedidos no guardan el teléfono, solo apuntan al cliente. En la hoja de cálculo habría que cambiar todas las filas de ese cliente — y basta olvidar una para que la hoja quede mintiendo.

2.

SELECT tc.table_name,
       string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) AS clave_primaria
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
  ON kcu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
  AND tc.table_schema = 'public'
GROUP BY tc.table_name
ORDER BY tc.table_name;
      table_name      |      clave_primaria
----------------------+---------------------------
 categories           | category_id
 customers            | customer_id
 employee_territories | employee_id, territory_id
 employees            | employee_id
 order_details        | order_id, product_id
 orders               | order_id
 products             | product_id
 region               | region_id
 shippers             | shipper_id
 suppliers            | supplier_id
 territories          | territory_id
 us_states            | state_id

3. Dos: order_details (order_id, product_id) y employee_territories (employee_id, territory_id). Ambas son tablas del medio de un "muchos a muchos". Lo que identifica una línea de pedido no es el pedido ni el producto por separado, sino la combinación: el producto 42 dentro del pedido 10248.

4. employee_territories: un empleado cubre varios territorios y un territorio lo atienden varios empleados.

Pregunta de negocio:

SELECT c.customer_id, c.company_name, c.country, c.phone
FROM customers c
LEFT JOIN orders o USING (customer_id)
WHERE o.order_id IS NULL;

Son 2 clientes. La clave es el LEFT JOIN: conserva todos los clientes, y los que no encontraron pedido quedan con NULL en las columnas de orders. Filtrar por ese NULL es preguntar "¿quiénes no tuvieron pareja?". Se ve en detalle en el tema anti-join-y-semi-join.